{"locale":"es","source":{"rawBase":"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/docs","githubBase":"https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs","editBase":"https://github.com/earendil-works/pi/edit/main/packages/coding-agent/docs"},"redirects":[{"from":"/docs/latest/session","to":"/docs/latest/session-format"},{"from":"/docs/latest/tree","to":"/docs/latest/sessions"}],"fileToSlug":{"compaction.md":"compaction","containerization.md":"containerization","custom-provider.md":"custom-provider","development.md":"development","environment-variables.md":"environment-variables","extensions.md":"extensions","index.md":"index","json.md":"json","keybindings.md":"keybindings","llama-cpp.md":"llama-cpp","models.md":"models","packages.md":"packages","prompt-templates.md":"prompt-templates","providers.md":"providers","quickstart.md":"quickstart","rpc.md":"rpc","sdk.md":"sdk","security.md":"security","session-format.md":"session-format","sessions.md":"sessions","settings.md":"settings","shell-aliases.md":"shell-aliases","skills.md":"skills","terminal-setup.md":"terminal-setup","termux.md":"termux","themes.md":"themes","tmux.md":"tmux","tui.md":"tui","usage.md":"usage","windows.md":"windows"},"pages":{"es":{"compaction":{"title":"Compactación y resumen de ramas","markdown":"Los LLM tienen ventanas de contexto limitadas. Cuando las conversaciones se alargan demasiado, Pi utiliza la compactación para resumir el contenido anterior y al mismo tiempo preservar el trabajo reciente. Esta página cubre tanto la autocompactación como branch summarization.\n\n**Archivos fuente** ([pi-mono](https://github.com/earendil-works/pi-mono)):\n- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) - Lógica de autocompactación\n- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) - Resumen de sucursales\n- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts) - Utilidades compartidas (seguimiento de archivos, serialización)\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - Tipos de entrada (`CompactionEntry`, `BranchSummaryEntry`)\n- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) - Tipos de eventos de extensión\n\nPara las definiciones de TypeScript en su proyecto, inspeccione `node_modules/@earendil-works/pi-coding-agent/dist/`.\n\n## Descripción general\n\nPi tiene dos mecanismos de resumen:\n\n| Mecanismo | Desencadenar | Objetivo |\n|-----------|---------|---------|\n| Compactación | El contexto supera el umbral, o `/compact` | Resumir mensajes antiguos para liberar contexto |\n| Resumen de sucursales | `/tree` navegación | Preservar el contexto al cambiar de sucursal |\n\nAmbos utilizan el mismo formato de resumen estructurado y realizan un seguimiento de las operaciones de archivos de forma acumulativa. Las solicitudes de compactación y resumen de bifurcación utilizan ID de sesión de enrutamiento nuevos y, cuando el proveedor las admite, deshabilitan las escrituras de caché de avisos porque es poco probable que estos avisos únicos se reutilicen.\n\n## Compactación\n\n### Cuando se activa\n\nLa compactación automática se activa cuando:\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\nDe forma predeterminada, `reserveTokens` son 16384 tokens (configurables en `~/.pi/agent/settings.json` o `<project-dir>/.pi/settings.json`). Esto deja espacio para la respuesta del LLM.\n\nTambién puedes activarlo manualmente con `/compact [instructions]`, donde las instrucciones opcionales centran el resumen.\n\n### Cómo funciona\n\n1. **Buscar punto de corte**: retroceda desde el mensaje más reciente, acumulando estimaciones de tokens hasta alcanzar `keepRecentTokens` (20k predeterminado, configurable en `~/.pi/agent/settings.json` o `<project-dir>/.pi/settings.json`).\n2. **Extraer mensajes**: recopile mensajes desde el límite mantenido anterior (o inicio de sesión) hasta el punto de corte.\n3. **Generar resumen**: Llame a LLM para resumir con formato estructurado, pasando el resumen anterior como contexto iterativo cuando esté presente.\n4. **Agregar entrada**: Guarde `CompactionEntry` con resumen y `firstKeptEntryId`\n5. **Recarga**: Se recarga la sesión, usando resumen + mensajes desde `firstKeptEntryId` en adelante\n\n```\nBefore compaction:\n\n  entry:  0     1     2     3      4     5     6      7      8     9\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘\n                └────────┬───────┘ └──────────────┬──────────────┘\n               messagesToSummarize            kept messages\n                                   ↑\n                          firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n  entry:  0     1     2     3      4     5     6      7      8     9     10\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘\n               └──────────┬──────┘ └──────────────────────┬───────────────────┘\n                 not sent to LLM                    sent to LLM\n                                                         ↑\n                                              starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n       ↑         ↑      └─────────────────┬────────────────┘\n    prompt   from cmp          messages from firstKeptEntryId\n```\n\nEn compactaciones repetidas, el tramo resumido comienza en el límite mantenido de la compactación anterior (`firstKeptEntryId`), no en la entrada de compactación en sí, y vuelve a la entrada después de la compactación anterior si esa entrada mantenida no se puede encontrar en el camino. Esto preserva los mensajes que sobrevivieron a la compactación anterior al incluirlos también en la siguiente pasada de resumen. Pi también vuelve a calcular `tokensBefore` a partir del contexto de sesión reconstruido antes de escribir el nuevo `CompactionEntry`, por lo que el recuento de tokens refleja el contexto real previo a la compactación que se reemplaza.\n\n### Giros divididos\n\nUn \"turno\" comienza con un mensaje de usuario e incluye todas las respuestas del asistente y llamadas de herramientas hasta el siguiente mensaje de usuario. Normalmente, la compactación corta en los límites de las curvas.\n\nCuando un solo giro excede `keepRecentTokens`, el punto de corte llega a mitad del giro en un mensaje del asistente. Este es un \"turno dividido\":\n\n```\nSplit turn (one huge turn exceeds budget):\n\n  entry:  0     1     2      3     4      5      6     7      8\n        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐\n        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │\n        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘\n                ↑                                     ↑\n         turnStartIndex = 1                  firstKeptEntryId = 7\n                │                                     │\n                └──── turnPrefixMessages (1-6) ───────┘\n                                                      └── kept (7-8)\n\n  isSplitTurn = true\n  messagesToSummarize = []  (no complete turns before)\n  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]\n```\n\nPara turnos divididos, Pi genera dos resúmenes y los fusiona:\n1. **Resumen del historial**: contexto anterior (si corresponde)\n2. **Resumen del prefijo de turno**: la primera parte del turno dividido\n\n### Reglas de punto de corte\n\nLos puntos de corte válidos son:\n- Mensajes de usuario\n- Mensajes del asistente\n- Mensajes de ejecución de Bash\n- Mensajes personalizados (mensaje_personalizado, resumen_rama)\n\nNunca corte los resultados de la herramienta (deben permanecer con su llamada de herramienta).\n\n### Estructura de entrada de compactación\n\nDefinido en [`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface CompactionEntry<T = unknown> {\n  type: \"compaction\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  firstKeptEntryId: string;\n  tokensBefore: number;\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default compaction uses this for details (from compaction.ts):\ninterface CompactionDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nExtensions puede almacenar cualquier dato serializable JSON en `details`. La compactación predeterminada rastrea las operaciones de archivos, pero las implementaciones de extensiones personalizadas pueden usar su propia estructura. Los resúmenes generados y proporcionados por la extensión almacenan su LLM `usage` cuando esté disponible, de modo que los totales de las sesiones incluyan el trabajo de resumen.\n\nConsulte [`prepareCompaction()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) y [`compact()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) para conocer la implementación. Para un resumen programático directo, `generateSummary()` devuelve el texto de resumen y `generateSummaryWithUsage()` devuelve `{ text, usage }`.\n\n## Resumen de sucursales\n\n### Cuando se activa\n\nCuando usas `/tree` para navegar a una rama diferente, Pi te ofrece resumir el trabajo que estás dejando. Esto inyecta contexto de la rama izquierda a la nueva rama.\n\n### Cómo funciona\n\n1. **Buscar ancestro común**: nodo más profundo compartido por posiciones antiguas y nuevas\n2. **Recopila entradas**: camina desde la hoja vieja hasta el ancestro común\n3. **Prepárese con el presupuesto**: incluya mensajes hasta el presupuesto simbólico (los más recientes primero)\n4. **Generar resumen**: Llame a LLM con formato estructurado\n5. **Agregar entrada**: Guardar `BranchSummaryEntry` en el punto de navegación\n\n```\nTree before navigation:\n\n         ┌─ B ─ C ─ D (old leaf, being abandoned)\n    A ───┤\n         └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n         ┌─ B ─ C ─ D\n    A ───┤\n         └─ E ─ F ─ [summary of B,C,D] (new leaf)\n```\n\n### Seguimiento de archivos acumulativos\n\nTanto la compactación como el branch summarization rastrean archivos de forma acumulativa. Al generar un resumen, pi extrae las operaciones de archivos de:\n- Llamadas a herramientas en los mensajes que se resumen\n- Compactación anterior o resumen de ramas `details` (si corresponde)\n\nEsto significa que el seguimiento de archivos se acumula en múltiples compactaciones o resúmenes de ramas anidadas, preservando el historial completo de archivos leídos y modificados.\n\n### RamaResumenEstructura de entrada\n\nDefinido en [`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface BranchSummaryEntry<T = unknown> {\n  type: \"branch_summary\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  fromId: string;      // Entry we navigated from\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default branch summarization uses this for details (from branch-summarization.ts):\ninterface BranchSummaryDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nAl igual que la compactación, las extensiones pueden almacenar datos personalizados en `details`.\n\nConsulte [`collectEntriesForBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), [`prepareBranchEntries()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) y [`generateBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) para conocer la implementación.\n\n## Formato de resumen\n\nTanto la compactación como branch summarization usan el mismo formato estructurado:\n\n```markdown\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements mentioned by user]\n\n## Progress\n### Done\n- [x] [Completed tasks]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [What should happen next]\n\n## Critical Context\n- [Data needed to continue]\n\n<read-files>\npath/to/file1.ts\npath/to/file2.ts\n</read-files>\n\n<modified-files>\npath/to/changed.ts\n</modified-files>\n```\n\n### Serialización de mensajes\n\nAntes del resumen, los mensajes se serializan en texto mediante [`serializeConversation()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts):\n\n```\n[User]: What they said\n[Assistant thinking]: Internal reasoning\n[Assistant]: Response text\n[Assistant tool calls]: read(path=\"foo.ts\"); edit(path=\"bar.ts\", ...)\n[Tool result]: Output from tool\n```\n\nEsto evita que el modelo lo trate como una conversación para continuar.\n\nLos resultados de la herramienta se truncan a 2000 caracteres durante la serialización. El contenido que supera ese límite se reemplaza con un marcador que indica cuántos caracteres se truncaron. Esto mantiene las solicitudes de resumen dentro de presupuestos simbólicos razonables, ya que los resultados de las herramientas (especialmente de `read` y `bash`) suelen ser los que más contribuyen al tamaño del contexto.\n\n## Resumen personalizado a través de Extensions\n\nExtensions puede interceptar y personalizar tanto la compactación como branch summarization. Consulte [`extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) para conocer las definiciones de tipos de eventos.\n\n### sesión_antes_compact\n\nDisparado antes de la autocompactación o `/compact`. Puede cancelar o proporcionar un resumen personalizado. Consulte `SessionBeforeCompactEvent` y `CompactionPreparation` en el archivo de tipos.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // preparation.messagesToSummarize - messages to summarize\n  // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)\n  // preparation.previousSummary - previous compaction summary\n  // preparation.fileOps - extracted file operations\n  // preparation.tokensBefore - context tokens before compaction\n  // preparation.firstKeptEntryId - where kept messages start\n  // preparation.settings - compaction settings\n\n  // branchEntries - all entries on current branch (for custom state)\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n  // signal - AbortSignal (pass to LLM calls)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"Your summary...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: { /* custom data */ },\n    }\n  };\n});\n```\n\n#### Convertir mensajes a texto\n\nPara generar un resumen con tu propio modelo, convierte mensajes a texto usando `serializeConversation`:\n\n```typescript\nimport { convertToLlm, serializeConversation } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation } = event;\n  \n  // Convert AgentMessage[] to Message[], then serialize to text\n  const conversationText = serializeConversation(\n    convertToLlm(preparation.messagesToSummarize)\n  );\n  // Returns:\n  // [User]: message text\n  // [Assistant thinking]: thinking content\n  // [Assistant]: response text\n  // [Assistant tool calls]: read(path=\"...\"); bash(command=\"...\")\n  // [Tool result]: output text\n\n  // Now send to your model for summarization\n  const { summary, usage } = await myModel.summarize(conversationText);\n  \n  return {\n    compaction: {\n      summary,\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      usage,\n    }\n  };\n});\n```\n\nConsulte [custom-compaction.ts](../examples/extensions/custom-compaction.ts) para ver un ejemplo completo utilizando un modelo diferente.\n\n### sesión_antes_árbol\n\nDisparado antes de la navegación `/tree`. Siempre se activa independientemente de si el usuario elige resumir. Puede cancelar la navegación o proporcionar un resumen personalizado.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n\n  // preparation.targetId - where we're navigating to\n  // preparation.oldLeafId - current position (being abandoned)\n  // preparation.commonAncestorId - shared ancestor\n  // preparation.entriesToSummarize - entries that would be summarized\n  // preparation.userWantsSummary - whether user chose to summarize\n\n  // Cancel navigation entirely:\n  return { cancel: true };\n\n  // Provide custom summary (only used if userWantsSummary is true):\n  if (preparation.userWantsSummary) {\n    return {\n      summary: {\n        summary: \"Your summary...\",\n        // usage: summaryResponse.usage, // Optional; included in session totals\n        details: { /* custom data */ },\n      }\n    };\n  }\n});\n```\n\nConsulte `SessionBeforeTreeEvent` y `TreePreparation` en el archivo de tipos.\n\n## Ajustes\n\nConfigure la compactación en `~/.pi/agent/settings.json` o `<project-dir>/.pi/settings.json`:\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n| Configuración | Por defecto | Descripción |\n|---------|---------|-------------|\n| `enabled` | `true` | Habilitar la autocompactación |\n| `reserveTokens` | `16384` | Tokens para reservar para la respuesta LLM |\n| `keepRecentTokens` | `20000` | Tokens recientes para conservar (no resumidos) |\n\nDesactive la compactación automática con `\"enabled\": false`. Aún puedes compactar manualmente con `/compact`.","sourceFile":"compaction.md"},"containerization":{"title":"Contenedorización","markdown":"Pi se ejecuta con todos los permisos de forma predeterminada, pero en algunos casos querrás tener más control sobre en qué directorios Pi puede escribir y qué accesos tiene.\n\nHay dos opciones generales. Tú puedes\n1. ejecutar todo el proceso `pi` dentro de un entorno aislado, o\n2. ejecute `pi` en el host y enrute la ejecución de la herramienta a un entorno aislado.\n\n## Elige un patrón\n\n| Patrón | que esta aislado | Lo mejor para | Notas |\n| --- | --- | --- | --- |\n| Gondolin extensión | Herramientas integradas y comandos `!` | Aislamiento de micro-VM local mientras se mantiene la autenticación en el host | Ver [`examples/extensions/gondolin/`](../examples/extensions/gondolin/). |\n| Llano Docker | Proceso `pi` completo en un contenedor local | Aislamiento local simple | Los proveedores API key ingresan al contenedor. |\n| OpenShell | Todo el proceso `pi` en un sandbox controlado por políticas | Gestionado local o remotamente sandbox | Requiere una puerta de enlace OpenShell |\n\nExtensions se ejecuta dondequiera que se ejecute el proceso `pi`. Si ejecuta el host `pi` con una extensión de enrutamiento de herramientas, otras herramientas de extensión personalizadas aún se ejecutan en el host a menos que también deleguen sus operaciones.\n\n## Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) es una micro-VM Linux local.\nUtilice [example extension](../examples/extensions/gondolin) cuando desee `pi` en el host, pero todas las herramientas integradas se dirigen a la VM.\n\nConfiguración:\n\n```bash\ncp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin\ncd ~/.pi/agent/extensions/gondolin\nnpm install --ignore-scripts\n```\n\nEjecute desde el proyecto que desea montar:\n\n```bash\ncd /path/to/project\npi -e ~/.pi/agent/extensions/gondolin\n```\n\nLa extensión monta el host cwd en `/workspace` en la VM y anula `read`, `write`, `edit`, `bash`, `grep`, `find` y `ls`.\nLos comandos del usuario `!` también se enrutan a la VM.\nLos cambios de archivos en `/workspace` se escriben en el host.\n\nRequisitos: Node.js >= 23.6.0 para `@earendil-works/gondolin`, más QEMU (requiere instalación a través de su administrador de paquetes).\n\n## Llano Docker\n\nEjecute todo el proceso `pi` en Docker cuando desee el límite de contenedor local más simple.\n\n`Dockerfile.pi`:\n\n```dockerfile\nFROM node:24-bookworm-slim\n\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\nWORKDIR /workspace\nENTRYPOINT [\"pi\"]\n```\n\nConstruir y ejecutar:\n\n```bash\ndocker build -t pi-sandbox -f Dockerfile.pi .\n\ndocker run --rm -it \\\n  -e ANTHROPIC_API_KEY \\\n  -v \"$PWD:/workspace\" \\\n  -v pi-agent-home:/root/.pi/agent \\\n  pi-sandbox\n```\n\nEl `-v \"$PWD:/workspace\"` monta su directorio actual en el contenedor en /workspace de modo que las lecturas y escrituras en `/workspace` dentro de Docker afecten directamente a sus archivos host, como en el ejemplo Gondolin.\n\nUtilice un volumen con nombre para `/root/.pi/agent` si desea configuraciones y sesiones locales de contenedor. Al montar su host `~/.pi/agent` se exponen los archivos de sesión y autenticación del host en el contenedor.\n\n## OpenShell\n\nUtilice [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) cuando desee un sandbox controlado por políticas con controles de sistema de archivos, procesos, redes, credenciales y de inferencia.\nOpenShell puede ejecutar sandboxes a través de una puerta de enlace local respaldada por Docker, Podman o un tiempo de ejecución de VM, o mediante una puerta de enlace remota de Kubernetes.\n\nCada sandbox requiere una puerta de enlace activa.\nRegístrese y seleccione uno antes de crear un sandbox:\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\nLanza `pi` dentro de un OpenShell sandbox:\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\nEn este patrón, todo el proceso `pi` se ejecuta dentro del sandbox.\nLas herramientas integradas, los comandos `!` y las herramientas de extensión se ejecutan dentro del límite OpenShell.\n\nSi la puerta de enlace es remota, los archivos del proyecto no se montan enlazados desde el host, lo que significa que las escrituras en sandbox no se reflejan en su máquina.\nClona el repositorio dentro de sandbox o usa los comandos de transferencia de archivos OpenShell:\n\n```bash\nopenshell sandbox upload pi-sandbox ./repo /workspace\nopenshell sandbox download pi-sandbox /workspace/repo ./repo-out\n```\n\nLos proveedores de OpenShell pueden mantener los modelos sin procesar API key fuera de sandbox.\nCuando se configura el enrutamiento de inferencia, el código dentro de sandbox puede llamar a `https://inference.local` y la puerta de enlace inyecta las credenciales del proveedor configurado en sentido ascendente.\nConfigure Pi para usar el punto final correspondiente compatible con OpenAI o Anthropic si desea que el tráfico modelo use esta ruta.","sourceFile":"containerization.md"},"custom-provider":{"title":"Personalizado Providers","markdown":"Extensions puede registrar proveedores de modelos personalizados a través de `pi.registerProvider()`. Esto permite:\n\n- **Proxies**: enrute solicitudes a través de proxies corporativos o puertas de enlace API\n- **Puntos finales personalizados**: utilice implementaciones de modelos privados o autohospedados\n- **OAuth/SSO**: agregar flujos de autenticación para proveedores empresariales\n- **Personalizado APIs**: implementar la transmisión para LLM APIs no estándar\n\n## Ejemplo Extensions\n\nVea estos ejemplos completos de proveedores:\n\n- [`examples/extensions/custom-provider-anthropic/`](../examples/extensions/custom-provider-anthropic/)\n- [`examples/extensions/custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/)\n\n## Tabla de contenido\n\n- [Example Extensions](#example-extensions)\n- [Quick Reference](#quick-reference)\n- [Override Existing Provider](#override-existing-provider)\n- [Register New Provider](#register-new-provider)\n- [Unregister Provider](#unregister-provider)\n- [OAuth Support](#oauth-support)\n- [Custom Streaming API](#custom-streaming-api)\n- [Context Overflow Errors](#context-overflow-errors)\n- [Testing Your Implementation](#testing-your-implementation)\n- [Config Reference](#config-reference)\n- [Model Definition Reference](#model-definition-reference)\n\n## Referencia rápida\n\nExtensions puede registrar un pi-ai completo `Provider` o utilizar el formulario de configuración de proveedor heredado. Prefiera un proveedor completo cuando se requiera autenticación personalizada, filtrado, actualización o comportamiento de transmisión. Pi compone `models.json` anula los proveedores nativos registrados.\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(createProvider({\n    id: \"native-local\",\n    name: \"Native Local\",\n    baseUrl: \"http://localhost:8080/v1\",\n    auth: {\n      apiKey: {\n        name: \"Local server API key\",\n        async login(interaction) {\n          return {\n            type: \"api_key\",\n            key: await interaction.prompt({ type: \"secret\", message: \"API key\" })\n          };\n        },\n        async resolve({ credential }) {\n          return credential?.key\n            ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n            : undefined;\n        }\n      }\n    },\n    models: [],\n    api: openAICompletionsApi()\n  }));\n\n  // Legacy provider-config form:\n  // Override baseUrl for existing provider\n  pi.registerProvider(\"anthropic\", {\n    baseUrl: \"https://proxy.example.com\"\n  });\n\n  // Register new provider with models\n  pi.registerProvider(\"my-provider\", {\n    name: \"My Provider\",\n    baseUrl: \"https://api.example.com\",\n    apiKey: \"$MY_API_KEY\",\n    api: \"openai-completions\",\n    models: [\n      {\n        id: \"my-model\",\n        name: \"My Model\",\n        reasoning: false,\n        input: [\"text\", \"image\"],\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n        contextWindow: 128000,\n        maxTokens: 4096\n      }\n    ]\n  });\n}\n```\n\nLa fábrica de extensiones también puede ser `async`. Para el descubrimiento dinámico de modelos, busque y registre modelos en la fábrica en lugar de `session_start`. pi espera a la fábrica antes de que continúe el inicio, por lo que el proveedor está disponible durante el inicio interactivo y para `pi --list-models`.\n\n## Anular proveedor existente\n\nEl caso de uso más simple: redirigir a un proveedor existente a través de un proxy.\n\n```typescript\n// All Anthropic requests now go through your proxy\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Add custom headers to OpenAI requests\npi.registerProvider(\"openai\", {\n  headers: {\n    \"X-Custom-Header\": \"value\"\n  }\n});\n\n// Both baseUrl and headers\npi.registerProvider(\"google\", {\n  baseUrl: \"https://ai-gateway.corp.com/google\",\n  headers: {\n    \"X-Corp-Auth\": \"$CORP_AUTH_TOKEN\"  // env var or literal\n  }\n});\n```\n\nCuando solo se proporcionan `baseUrl` y/o `headers` (no `models`), todos los modelos existentes para ese proveedor se conservan con el nuevo punto final.\n\n## Registrar nuevo proveedor\n\nPara agregar un proveedor completamente nuevo, especifique `models` junto con la configuración requerida.\n\nSi la lista de modelos proviene de un punto final remoto, use una fábrica de extensiones asíncrona:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\nEsto registra los modelos recuperados antes de que finalice el inicio.\n\n```typescript\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",  // env var reference\n  api: \"openai-completions\",  // which streaming API to use\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,        // supports extended thinking\n      input: [\"text\", \"image\"],\n      cost: {\n        input: 3.0,           // $/million tokens\n        output: 15.0,\n        cacheRead: 0.3,\n        cacheWrite: 3.75\n      },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n```\n\nCuando se proporciona `models`, **reemplaza** todos los modelos existentes para ese proveedor.\n\n`apiKey` y los valores de encabezado personalizados usan la misma sintaxis de valor de configuración que `models.json`: `!command` al principio ejecuta un comando para el valor completo, `$ENV_VAR` y `${ENV_VAR}` interpolan variables de entorno, `$` emite un literal ``apiKey` y los valores de encabezado personalizados usan la misma sintaxis de valor de configuración que `models.json`: `!command` al principio ejecuta un comando para el valor completo, `$ENV_VAR` y `${ENV_VAR}` interpolan variables de entorno, `$` emite un literal  y `$!` emite un literal `!`.\n\n## Darse de baja del proveedor\n\nUtilice `pi.unregisterProvider(name)` para eliminar un proveedor que se registró previamente mediante `pi.registerProvider(name,...)`:\n\n```typescript\n// Register\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",\n  api: \"openai-completions\",\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,\n      input: [\"text\", \"image\"],\n      cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Later, remove it\npi.unregisterProvider(\"my-llm\");\n```\n\nAl cancelar el registro se eliminan los modelos dinámicos de ese proveedor, el respaldo API key, el registro de proveedor OAuth y los registros de controladores de flujo personalizados. Se restauran todos los modelos integrados o comportamiento del proveedor que se anularon.\n\nLas llamadas realizadas después de la fase de carga de extensión inicial se aplican inmediatamente, por lo que no se requiere `/reload`.\n\n### API Tipos\n\nEl campo `api` determina qué implementación de transmisión se utiliza:\n\n| API | Usar para |\n|-----|---------|\n| `anthropic-messages` | Claude antrópico API y compatibles |\n| `openai-completions` | Finalizaciones de OpenAI Chat API y compatibles |\n| `openai-responses` | Respuestas de OpenAI API |\n| `azure-openai-responses` | Respuestas de Azure OpenAI API |\n| `openai-codex-responses` | Respuestas del Códice OpenAI API |\n| `mistral-conversations` | Transmisión de terminaciones de chat nativo de Mistral |\n| `google-generative-ai` | IA generativa de Google API |\n| `google-vertex` | Google Vertex AI API |\n| `bedrock-converse-stream` | converse amazonas API |\n\nLa mayoría de los proveedores compatibles con OpenAI funcionan con `openai-completions`. Utilice el nivel de modelo `thinkingLevelMap` para niveles de pensamiento específicos del modelo y `compat` para las peculiaridades del proveedor. Los niveles `xhigh` y `max` son opcionales, requieren entradas de mapa no nulas y pueden estar separados por agujeros no admitidos:\n\n```typescript\nmodels: [{\n  id: \"custom-model\",\n  // ...\n  reasoning: true,\n  thinkingLevelMap: {              // map pi levels to provider values; null hides unsupported levels\n    minimal: null,\n    low: null,\n    medium: null,\n    high: \"default\",\n    xhigh: null,\n    max: \"max\"\n  },\n  compat: {\n    supportsDeveloperRole: false,   // use \"system\" instead of \"developer\"\n    supportsReasoningEffort: true,\n    maxTokensField: \"max_tokens\",   // instead of \"max_completion_tokens\"\n    requiresToolResultName: true,   // tool results need name field\n    thinkingFormat: \"qwen\",        // top-level enable_thinking: true\n    cacheControlFormat: \"anthropic\" // Anthropic-style cache_control markers\n  }\n}]\n```\n\nUtilice `openrouter` para controles `reasoning: { effort }` estilo OpenRouter. Utilice `together` para controles `reasoning: { enabled }` estilo Together; con `supportsReasoningEffort`, también envía `reasoning_effort`. Utilice `qwen-chat-template` para servidores locales compatibles con Qwen que lean `chat_template_kwargs.enable_thinking` y necesiten `preserve_thinking`.\nUtilice `cacheControlFormat: \"anthropic\"` para proveedores compatibles con OpenAI que exponen el almacenamiento en caché de mensajes de estilo Anthropic a través de `cache_control` en el mensaje del sistema, la última definición de herramienta y el contenido de texto del último usuario, asistente o resultado de la herramienta.\n\nPara proveedores compatibles con Anthropic que utilizan `api: \"anthropic-messages\"`, establezca `compat.forceAdaptiveThinking: true` en modelos o proveedores cuyo modelo ascendente requiere pensamiento adaptativo (`thinking.type: \"adaptive\"` más `output_config.effort`). Los modelos Claude adaptables incorporados configuran esto automáticamente. Configure `compat.allowEmptySignature: true` solo para proveedores que emiten firmas de pensamiento vacías y esperan `signature: \"\"` en la reproducción.\n\n> Nota de migración: Mistral pasó de `openai-completions` a `mistral-conversations`.\n> Utilice `mistral-conversations` para modelos Mistral nativos.\n> Si enruta intencionalmente puntos finales personalizados/compatibles con Mistral a través de `openai-completions`, configure los indicadores `compat` explícitamente según sea necesario.\n\n### Encabezado de autenticación\n\nSi su proveedor espera `Authorization: Bearer <key>` pero no utiliza un estándar API, establezca `authHeader: true`:\n\n```typescript\npi.registerProvider(\"custom-api\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  authHeader: true,  // adds Authorization: Bearer header\n  api: \"openai-completions\",\n  models: [...]\n});\n```\n\nLa clave se resuelve para cada solicitud. Un encabezado de solicitud explícita `Authorization` tiene prioridad sobre el valor generado.\n\n## OAuth Soporte\n\nAgregue autenticación OAuth/SSO que se integra con `/login`:\n\n```typescript\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"@earendil-works/pi-ai\";\n\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com/v1\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n\n    async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n      const method = await callbacks.onSelect({\n        message: \"Select login method:\",\n        options: [\n          { id: \"browser\", label: \"Browser OAuth\" },\n          { id: \"device\", label: \"Device code\" }\n        ]\n      });\n      if (!method) throw new Error(\"Login cancelled\");\n\n      let code: string;\n      if (method === \"device\") {\n        callbacks.onDeviceCode({\n          userCode: \"ABCD-1234\",\n          verificationUri: \"https://sso.corp.com/device\",\n          intervalSeconds: 5,\n          expiresInSeconds: 900\n        });\n        code = await pollDeviceCodeUntilComplete();\n      } else {\n        callbacks.onAuth({ url: \"https://sso.corp.com/authorize?...\" });\n        code = await callbacks.onPrompt({ message: \"Enter SSO code:\" });\n      }\n\n      // Exchange for tokens (your implementation)\n      const tokens = await exchangeCodeForTokens(code);\n\n      return {\n        refresh: tokens.refreshToken,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    async refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials> {\n      const tokens = await refreshAccessToken(credentials.refresh, signal);\n      return {\n        refresh: tokens.refreshToken ?? credentials.refresh,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    getApiKey(credentials: OAuthCredentials): string {\n      return credentials.access;\n    }\n  }\n});\n```\n\nDespués del registro, los usuarios pueden autenticarse a través de `/login corporate-ai`.\n\n### OAuthIniciar sesiónDevoluciones de llamada\n\nEl objeto `callbacks` proporciona interacciones neutrales en la interfaz de usuario para el flujo propiedad del proveedor:\n\n```typescript\ninterface OAuthLoginCallbacks {\n  // Open URL in browser (for OAuth redirects)\n  onAuth(params: { url: string }): void;\n\n  // Show device code (for device authorization flow)\n  onDeviceCode(params: {\n    userCode: string;\n    verificationUri: string;\n    intervalSeconds?: number;\n    expiresInSeconds?: number;\n  }): void;\n\n  // Show transient progress\n  onProgress?(message: string): void;\n\n  // Prompt user for input (for manual token entry)\n  onPrompt(params: { message: string }): Promise<string>;\n\n  // Show an interactive selector, e.g. to choose browser OAuth vs device code\n  onSelect(params: {\n    message: string;\n    options: { id: string; label: string }[];\n  }): Promise<string | undefined>;\n}\n```\n\n### OAuthCredenciales\n\nLas credenciales persisten en `~/.pi/agent/auth.json`:\n\n```typescript\ninterface OAuthCredentials {\n  refresh: string;   // Refresh token (for refreshToken())\n  access: string;    // Access token (returned by getApiKey())\n  expires: number;   // Expiration timestamp in milliseconds\n}\n```\n\n## Transmisión personalizada API\n\nPara proveedores con API no estándar, implemente `streamSimple`. Estudie las implementaciones de proveedores existentes antes de escribir la suya propia:\n\n**Implementaciones de referencia:**\n- [anthropic.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) - Mensajes Antrópicos API\n- [mistral.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/mistral.ts) - Conversaciones Mistral API\n- [openai-completions.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) - Finalizaciones del chat OpenAI\n- [openai-responses.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-responses.ts) - Respuestas de OpenAI API\n- [google.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/google.ts) - IA generativa de Google\n- [amazon-bedrock.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) - Base de AWS\n\n### Patrón de corriente\n\nTodos los proveedores siguen el mismo patrón:\n\n```typescript\nimport {\n  type AssistantMessage,\n  type AssistantMessageEventStream,\n  type Context,\n  type Model,\n  type SimpleStreamOptions,\n  calculateCost,\n  createAssistantMessageEventStream,\n} from \"@earendil-works/pi-ai\";\n\nfunction streamMyProvider(\n  model: Model<any>,\n  context: Context,\n  options?: SimpleStreamOptions\n): AssistantMessageEventStream {\n  const stream = createAssistantMessageEventStream();\n\n  (async () => {\n    // Initialize output message\n    const output: AssistantMessage = {\n      role: \"assistant\",\n      content: [],\n      api: model.api,\n      provider: model.provider,\n      model: model.id,\n      usage: {\n        input: 0,\n        output: 0,\n        cacheRead: 0,\n        cacheWrite: 0,\n        totalTokens: 0,\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n      },\n      stopReason: \"pending\",\n      timestamp: Date.now(),\n    };\n\n    try {\n      // Push start event\n      stream.push({ type: \"start\", partial: output });\n\n      // Make API request and process response...\n      // Push content events as they arrive and set stopReason from the terminal event.\n      if (output.stopReason === \"pending\") {\n        throw new Error(\"Provider stream ended without a stop reason\");\n      }\n      if (output.stopReason === \"error\" || output.stopReason === \"aborted\") {\n        throw new Error(output.errorMessage || \"An unknown error occurred\");\n      }\n\n      // Push done event\n      stream.push({\n        type: \"done\",\n        reason: output.stopReason,\n        message: output\n      });\n      stream.end();\n    } catch (error) {\n      output.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n      output.errorMessage = error instanceof Error ? error.message : String(error);\n      stream.push({ type: \"error\", reason: output.stopReason, error: output });\n      stream.end();\n    }\n  })();\n\n  return stream;\n}\n```\n\n### Tipos de eventos\n\nEnvíe eventos a través de `stream.push()` en este orden:\n\n1. `{ type: \"start\", partial: output }` - Transmisión iniciada\n\n2. Eventos de contenido (repetibles, seguimiento `contentIndex` para cada bloque):\n   - `{ type: \"text_start\", contentIndex, partial }` - Bloque de texto iniciado\n   - `{ type: \"text_delta\", contentIndex, delta, partial }` - Fragmento de texto\n   - `{ type: \"text_end\", contentIndex, content, partial }` - Bloque de texto finalizado\n   - `{ type: \"thinking_start\", contentIndex, partial }` - El pensamiento comenzó\n   - `{ type: \"thinking_delta\", contentIndex, delta, partial }` - Fragmento de pensamiento\n   - `{ type: \"thinking_end\", contentIndex, content, partial }` - Se acabó el pensamiento\n   - `{ type: \"toolcall_start\", contentIndex, partial }` - Se inició la llamada a la herramienta\n   - `{ type: \"toolcall_delta\", contentIndex, delta, partial }` - Llamada a herramienta JSON fragmento\n   - `{ type: \"toolcall_end\", contentIndex, toolCall, partial }` - Llamada de herramienta finalizada\n\n3. `{ type: \"done\", reason, message }` o `{ type: \"error\", reason, error }` - Transmisión finalizada\n\nEl campo `partial` en cada evento contiene el estado `AssistantMessage` actual. Actualice `output.content` a medida que reciba datos, luego incluya `output` como `partial`.\n\n### Bloques de contenido\n\nAgregue bloques de contenido a `output.content` a medida que lleguen:\n\n```typescript\n// Text block\noutput.content.push({ type: \"text\", text: \"\" });\nstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\n// As text arrives\nconst block = output.content[contentIndex];\nif (block.type === \"text\") {\n  block.text += delta;\n  stream.push({ type: \"text_delta\", contentIndex, delta, partial: output });\n}\n\n// When block completes\nstream.push({ type: \"text_end\", contentIndex, content: block.text, partial: output });\n```\n\n### Llamadas de herramientas\n\nLas llamadas a herramientas requieren acumular JSON y analizar:\n\n```typescript\n// Start tool call\noutput.content.push({\n  type: \"toolCall\",\n  id: toolCallId,\n  name: toolName,\n  arguments: {}\n});\nstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\n// Accumulate JSON\nlet partialJson = \"\";\npartialJson += jsonDelta;\ntry {\n  block.arguments = JSON.parse(partialJson);\n} catch {}\nstream.push({ type: \"toolcall_delta\", contentIndex, delta: jsonDelta, partial: output });\n\n// Complete\nstream.push({\n  type: \"toolcall_end\",\n  contentIndex,\n  toolCall: { type: \"toolCall\", id, name, arguments: block.arguments },\n  partial: output\n});\n```\n\n### Uso y costo\n\nActualice el uso desde la respuesta API y calcule el costo:\n\n```typescript\noutput.usage.input = response.usage.input_tokens;\noutput.usage.output = response.usage.output_tokens;\noutput.usage.cacheRead = response.usage.cache_read_tokens ?? 0;\noutput.usage.cacheWrite = response.usage.cache_write_tokens ?? 0;\noutput.usage.totalTokens = output.usage.input + output.usage.output +\n                           output.usage.cacheRead + output.usage.cacheWrite;\ncalculateCost(model, output.usage);\n```\n\n### Errores de desbordamiento de contexto\n\nCuando una solicitud excede la ventana de contexto del modelo, pi puede recuperarse automáticamente compactando la conversación y volviendo a intentarlo. Esta recuperación solo se activa si pi reconoce la falla como un desbordamiento.\n\nLa detección se ejecuta en el mensaje del asistente finalizado:\n\n- `stopReason === \"error\"`\n- `errorMessage` coincide con uno de los patrones de desbordamiento conocidos de pi (ver [`packages/ai/src/utils/overflow.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/utils/overflow.ts))\n\nSi su proveedor devuelve errores de desbordamiento con un mensaje que pi no reconoce, normalice el error desde la misma extensión que registra el proveedor. Utilice un controlador `message_end` para reescribir el mensaje del asistente de modo que `errorMessage` comience con una frase que pi reconozca. La alternativa genérica `context_length_exceeded` es la opción más segura.\n\n```typescript\nconst MY_PROVIDER_OVERFLOW_PATTERN = /your provider's overflow phrase/i;\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"my-provider\", { /* ... */ });\n\n  pi.on(\"message_end\", (event, ctx) => {\n    const message = event.message;\n    if (message.role !== \"assistant\") return;\n    if (message.stopReason !== \"error\") return;\n    if (\n      message.provider !== \"my-provider\" &&\n      ctx.model?.provider !== \"my-provider\"\n    )\n      return;\n\n    const errorMessage = message.errorMessage ?? \"\";\n    if (errorMessage.includes(\"context_length_exceeded\")) return;\n    if (!MY_PROVIDER_OVERFLOW_PATTERN.test(errorMessage)) return;\n\n    return {\n      message: {\n        ...message,\n        errorMessage: `context_length_exceeded: ${errorMessage}`,\n      },\n    };\n  });\n}\n```\n\n`message_end` se ejecuta antes de que pi rastree el mensaje del asistente para la autocompactación, por lo que el `errorMessage` reescrito es lo que pi verifica. Con esto en su lugar, pi:\n\n1. Detecta el desbordamiento de `errorMessage`.\n2. Suelta el mensaje del asistente fallido desde el contexto en vivo.\n3. Ejecute la compactación.\n4. Vuelva a intentar la solicitud una vez.\n\nGuarde la reescritura con cuidado:\n\n- Ámbite a tu proveedor (`message.provider` y `ctx.model?.provider`) para que los errores no relacionados de otros proveedores no se modifiquen.\n- Haga coincidir un patrón específico del proveedor, no los patrones de desbordamiento genéricos de pi. Reescribir los errores de límite de velocidad o limitación (`rate limit`, `too many requests`) activaría falsamente la compactación en lugar de la ruta normal de reintento con retroceso de pi.\n- Omita cuando `errorMessage` ya incluya `context_length_exceeded` para que el controlador sea idempotente.\n\n### Registro\n\nRegistre su función de transmisión:\n\n```typescript\npi.registerProvider(\"my-provider\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  api: \"my-custom-api\",\n  models: [...],\n  streamSimple: streamMyProvider\n});\n```\n\n## Probando su implementación\n\nPruebe su proveedor con los mismos conjuntos de pruebas utilizados por los proveedores integrados. Copie y adapte estos archivos de prueba desde [packages/ai/test/](https://github.com/earendil-works/pi-mono/tree/main/packages/ai/test):\n\n| Prueba | Objetivo |\n|------|---------|\n| `stream.test.ts` | Transmisión básica, salida de texto |\n| `tokens.test.ts` | Recuento y uso de tokens |\n| `abort.test.ts` | Abortar Manejo de señales |\n| `empty.test.ts` | Respuestas vacías/mínimas |\n| `context-overflow.test.ts` | Límites de la ventana de contexto |\n| `image-limits.test.ts` | Manejo de entrada de imágenes |\n| `unicode-surrogate.test.ts` | Casos extremos Unicode |\n| `tool-call-without-result.test.ts` | Casos extremos de llamada de herramientas |\n| `image-tool-result.test.ts` | Imágenes en resultados de herramientas |\n| `total-tokens.test.ts` | Cálculo total de tokens |\n| `cross-provider-handoff.test.ts` | Transferencia de contexto entre proveedores |\n\nEjecute pruebas con sus pares de proveedor/modelo para verificar la compatibilidad.\n\n## Referencia de configuración\n\n```typescript\ninterface ProviderConfig {\n  /** Display name for the provider in UI such as /login. */\n  name?: string;\n\n  /** API endpoint URL. Required when defining models. */\n  baseUrl?: string;\n\n  /** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or !command. Required when defining models (unless oauth). */\n  apiKey?: string;\n\n  /** API type for streaming. Required at provider or model level when defining models. */\n  api?: Api;\n\n  /** Custom streaming implementation for non-standard APIs. */\n  streamSimple?: (\n    model: Model<Api>,\n    context: Context,\n    options?: SimpleStreamOptions\n  ) => AssistantMessageEventStream;\n\n  /** Custom headers to include in requests. Values use the same resolution syntax as apiKey. */\n  headers?: Record<string, string>;\n\n  /** If true, adds Authorization: Bearer header with the resolved API key. */\n  authHeader?: boolean;\n\n  /** Models to register. If provided, replaces all existing models for this provider. */\n  models?: ProviderModelConfig[];\n\n  /** OAuth provider for /login support. */\n  oauth?: {\n    name: string;\n    login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;\n    refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials>;\n    getApiKey(credentials: OAuthCredentials): string;\n  };\n}\n```\n\n## Referencia de definición de modelo\n\n```typescript\ninterface ProviderModelConfig {\n  /** Model ID (e.g., \"claude-sonnet-4-20250514\"). */\n  id: string;\n\n  /** Display name (e.g., \"Claude 4 Sonnet\"). */\n  name: string;\n\n  /** API type override for this specific model. */\n  api?: Api;\n\n  /** API endpoint URL override for this specific model. */\n  baseUrl?: string;\n\n  /** Whether the model supports extended thinking. */\n  reasoning: boolean;\n\n  /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */\n  thinkingLevelMap?: Partial<Record<\"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\", string | null>>;\n\n  /** Supported input types. */\n  input: (\"text\" | \"image\")[];\n\n  /** Cost per million tokens (for usage tracking). */\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n  };\n\n  /** Maximum context window size in tokens. */\n  contextWindow: number;\n\n  /** Maximum output tokens. */\n  maxTokens: number;\n\n  /** Custom headers for this specific model. */\n  headers?: Record<string, string>;\n\n  /** Compatibility settings for the selected API. */\n  compat?: {\n    // openai-completions\n    supportsStore?: boolean;\n    supportsDeveloperRole?: boolean;\n    supportsReasoningEffort?: boolean;\n    supportsUsageInStreaming?: boolean;\n    supportsFinishReason?: boolean;\n    supportsStrictMode?: boolean;\n    supportsOpenAIGrammarTools?: boolean; // openai-completions/openai-responses; false falls back to normal function tools\n    maxTokensField?: \"max_completion_tokens\" | \"max_tokens\";\n    requiresToolResultName?: boolean;\n    requiresAssistantAfterToolResult?: boolean;\n    requiresThinkingAsText?: boolean;\n    requiresReasoningContentOnAssistantMessages?: boolean;\n    thinkingFormat?: \"openai\" | \"openrouter\" | \"deepseek\" | \"together\" | \"baseten\" | \"zai\" | \"qwen\" | \"chat-template\" | \"qwen-chat-template\" | \"string-thinking\" | \"ant-ling\";\n    chatTemplateKwargs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    chatTemplateArgs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    cacheControlFormat?: \"anthropic\";\n    sessionAffinityFormat?: \"openai\" | \"openai-nosession\" | \"openrouter\";\n    sendSessionAffinityHeaders?: boolean;\n\n    // anthropic-messages\n    supportsEagerToolInputStreaming?: boolean;\n    supportsLongCacheRetention?: boolean;\n    sendSessionAffinityHeaders?: boolean;\n    supportsCacheControlOnTools?: boolean;\n    forceAdaptiveThinking?: boolean;\n    allowEmptySignature?: boolean;\n    supportsStrictTools?: boolean;\n  };\n}\n```\n\n`openrouter` envía `reasoning: { effort }`. `deepseek` envía `thinking: { type: \"enabled\" | \"disabled\" }` y `reasoning_effort` cuando está habilitado. `together` envía `reasoning: { enabled }` y también `reasoning_effort` cuando `supportsReasoningEffort` está habilitado. `qwen` es para el nivel superior estilo DashScope `enable_thinking`. Utilice `qwen-chat-template` para servidores locales compatibles con Qwen que lean `chat_template_kwargs.enable_thinking` y necesiten `preserve_thinking`. Utilice `chat-template` para `chat_template_kwargs` configurable, por ejemplo DeepSeek V3.x detrás de vLLM con `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }`. Utilice `thinkingFormat: \"baseten\"` con `chatTemplateArgs` cuando el proveedor espere valores de alternancia inferiores a `chat_template_args` y, opcionalmente, admita el nivel superior `reasoning_effort`.\n`cacheControlFormat: \"anthropic\"` aplica marcadores `cache_control` de estilo antrópico al mensaje del sistema, a la última definición de herramienta y al contenido de texto del último usuario, asistente o resultado de herramienta.","sourceFile":"custom-provider.md"},"development":{"title":"Desarrollo","markdown":"Consulte [AGENTS.md](https://github.com/earendil-works/pi-mono/blob/main/AGENTS.md) para obtener pautas adicionales.\n\n## Configuración\n\n```bash\ngit clone https://github.com/earendil-works/pi-mono\ncd pi-mono\nnpm install\nnpm run build\n```\n\nEjecutar desde la fuente:\n\n```bash\n/path/to/pi-mono/pi-test.sh\n```\n\nEl script se puede ejecutar desde cualquier directorio. Pi mantiene el directorio de trabajo actual de la persona que llama.\n\n## Bifurcación / Cambio de marca\n\nConfigurar vía `package.json`:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"pi\",\n    \"configDir\": \".pi\"\n  }\n}\n```\n\nCambie los campos `name`, `configDir` y `bin` para su bifurcación. Afecta al banner CLI, las rutas de configuración y los nombres de las variables de entorno.\n\n## Resolución de ruta\n\nTres modos de ejecución: npm instalación, binario independiente, tsx desde la fuente.\n\n**Utilice siempre `src/config.ts`** para los recursos del paquete:\n\n```typescript\nimport { getPackageDir, getThemeDir } from \"./config.js\";\n```\n\nNunca use `__dirname` directamente para los activos del paquete.\n\n## Comando de depuración\n\n`/debug` (oculto) escribe en `~/.pi/agent/pi-debug.log`:\n- Líneas TUI renderizadas con códigos ANSI\n- Últimos mensajes enviados al LLM\n\n## Pruebas\n\n```bash\n./test.sh                         # Run non-LLM tests (no API keys needed)\nnpm test                          # Run all tests\nnpm test -- test/specific.test.ts # Run specific test\n```\n\n## Estructura del proyecto\n\n```\npackages/\n  ai/           # LLM provider abstraction\n  agent/        # Agent loop and message types  \n  tui/          # Terminal UI components\n  coding-agent/ # CLI and interactive mode\n```","sourceFile":"development.md"},"environment-variables":{"title":"Variables de entorno","markdown":"Pi utiliza variables de entorno de tres maneras:\n\n- Variables como `PI_OFFLINE` configuran el proceso Pi.\n- Pi establece `PI_CODING_AGENT` para que los procesos secundarios puedan detectar que se ejecutan dentro de Pi.\n- Los comandos ejecutados por la herramienta bash invocable por LLM reciben variables `PI_*` que describen la sesión actual.\n\nLas variables clave del proveedor API se documentan por separado en [Providers](providers.md#environment-variables-or-auth-file).\n\n## Marcador de proceso\n\nLos puntos de entrada CLI y RPC se establecen en `PI_CODING_AGENT=true`. Los procesos secundarios lo heredan y pueden usarlo para detectar que se ejecutan dentro de Pi. No es específico de la sesión y no se configura automáticamente cuando Pi se incrusta a través de SDK.\n\n## Entorno de sesión de la herramienta Bash\n\nLos comandos ejecutados por la herramienta bash reciben el estado actual de la sesión Pi:\n\n| Variable | Descripción |\n|----------|-------------|\n| `PI_SESSION_ID` | ID de sesión actual |\n| `PI_SESSION_FILE` | Ruta absoluta al archivo JSONL de la sesión actual; Desarmado para sesiones efímeras. |\n| `PI_PROVIDER` | Proveedor de modelo actualmente seleccionado |\n| `PI_MODEL` | ID del modelo seleccionado actualmente |\n| `PI_REASONING_LEVEL` | Nivel de razonamiento efectivo actual: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` o `max` |\n\nLos valores se resuelven cuando se inicia cada comando. Por lo tanto, cambiar de modelo o cambiar el nivel de razonamiento afecta al siguiente comando bash sin reiniciar Pi. `PI_PROVIDER` y `PI_MODEL` identifican el modelo Pi seleccionado, no un modelo ascendente diferente que un enrutador puede elegir internamente.\n\nCuando se le pregunte qué modelo o proveedor se está ejecutando, inspeccione estas variables en lugar de inferir la respuesta del mensaje del sistema:\n\n```bash\nprintf '%s/%s\\n' \"$PI_PROVIDER\" \"$PI_MODEL\"\nprintf 'reasoning=%s session=%s\\n' \"$PI_REASONING_LEVEL\" \"$PI_SESSION_ID\"\n```\n\nEl archivo de sesión se puede inspeccionar directamente cuando la sesión es persistente:\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\nEstas variables se inyectan en la herramienta bash invocable por LLM. No se inyectan en los comandos `!` o `!!` ingresados ​​por el usuario.\n\n### Herramientas Bash personalizadas\n\nLas herramientas Bash creadas con `createBashTool()` exponen el entorno de sesión de forma predeterminada cuando se registran con Pi. La inyección ocurre antes de `spawnHook`, por lo que un gancho recibe las variables en `ctx.env`:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\nDeshabilite los metadatos de la sesión independientemente del gancho de generación:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\nCuando está deshabilitado, Pi elimina los valores heredados de estas variables, de modo que los procesos Pi anidados no exponen metadatos obsoletos de la sesión principal.\n\n## Pi Configuración del proceso\n\nEstas variables son leídas por el propio Pi:\n\n| Variable | Descripción |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | Anule el directorio de configuración; el valor predeterminado es `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | Anular el almacenamiento de sesiones; anulado por `--session-dir` |\n| `PI_PACKAGE_DIR` | Anular el directorio del paquete, útil para las rutas de almacenamiento de Nix/Guix |\n| `PI_OFFLINE` | Deshabilite las operaciones de red de inicio, incluidas las comprobaciones de actualizaciones, las actualizaciones de paquetes y la telemetría de instalación/actualización. |\n| `PI_SKIP_VERSION_CHECK` | Deshabilite la solicitud de la última versión `pi.dev` |\n| `PI_TELEMETRY` | Anular la telemetría de instalación/actualización y los encabezados de atribución de proveedores: `1`/`true`/`yes` o `0`/`false`/`no` |\n| `PI_CACHE_RETENTION` | Establezca en `long` para el almacenamiento en caché extendido del aviso del proveedor cuando sea compatible |\n| `PI_SHARE_VIEWER_URL` | Anule la URL base utilizada por `/share` |\n| `PI_HARDWARE_CURSOR` | Establezca en `1` para mostrar el cursor de hardware; ver [Terminal setup](terminal-setup.md) |\n| `VISUAL`, `EDITOR` | Respaldo del editor externo cuando `externalEditor` no está configurado |\n| `HTTP_PROXY`, `HTTPS_PROXY` | Solicitudes HTTP salientes de proxy |\n\nLas credenciales del proveedor como `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` y la configuración del proveedor de la nube se enumeran en [Providers](providers.md#environment-variables-or-auth-file).","sourceFile":"environment-variables.md"},"extensions":{"title":"Extensions","markdown":"> pi puede crear extensiones. Pídale que cree uno para su caso de uso.\n\n\nExtensions 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.\n\n> **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`.\n\n**Capacidades clave:**\n- **Herramientas personalizadas**: registre herramientas a las que el LLM puede llamar a través de `pi.registerTool()`\n- **Interceptación de eventos**: bloquear o modificar llamadas de herramientas, inyectar contexto, personalizar la compactación\n- **Interacción del usuario**: avisar a los usuarios mediante `ctx.ui` (seleccionar, confirmar, ingresar, notificar)\n- **Componentes de interfaz de usuario personalizados**: componentes TUI completos con entrada de teclado a través de `ctx.ui.custom()` para interacciones complejas\n- **Comandos personalizados**: registre comandos como `/mycommand` mediante `pi.registerCommand()`\n- **Persistencia de la sesión**: estado de la tienda que sobrevive a los reinicios mediante `pi.appendEntry()`\n- **Representación personalizada**: controle cómo aparecen las llamadas/resultados de herramientas y los mensajes en TUI\n\n**Casos de uso de ejemplo:**\n- Puertas de permiso (confirmar antes de `rm -rf`, `sudo`, etc.)\n- Git puntos de control (guardar en cada turno, restaurar en la rama)\n- Protección de ruta (bloque de escritura en `.env`, `node_modules/`)\n- Compactación personalizada (resume la conversación a tu manera)\n- Resúmenes de conversaciones (ver ejemplo `summarize.ts`)\n- Herramientas interactivas (preguntas, asistentes, cuadros de diálogo personalizados)\n- Herramientas con estado (listas de tareas pendientes, grupos de conexiones)\n- Integraciones externas (observadores de archivos, webhooks, activadores de CI)\n- Juegos mientras esperas (ver ejemplo `snake.ts`)\n\nConsulte [examples/extensions/](../examples/extensions/) para conocer implementaciones funcionales.\n\n## Tabla de contenido\n\n- [Quick Start](#quick-start)\n- [Extension Locations](#extension-locations)\n- [Available Imports](#available-imports)\n- [Writing an Extension](#writing-an-extension)\n  - [Extension Styles](#extension-styles)\n- [Events](#events)\n  - [Lifecycle Overview](#lifecycle-overview)\n  - [Resource Events](#resource-events)\n  - [Session Events](#session-events)\n  - [Agent Events](#agent-events)\n  - [Model Events](#model-events)\n  - [Tool Events](#tool-events)\n- [ExtensionContext](#extensioncontext)\n- [ExtensionCommandContext](#extensioncommandcontext)\n- [ExtensionAPI Methods](#extensionapi-methods)\n- [State Management](#state-management)\n- [Custom Tools](#custom-tools)\n  - [Dynamic Tool Loading](#dynamic-tool-loading)\n- [Custom UI](#custom-ui)\n- [Error Handling](#error-handling)\n- [Mode Behavior](#mode-behavior)\n- [Examples Reference](#examples-reference)\n\n## Inicio rápido\n\nCrear `~/.pi/agent/extensions/my-extension.ts`:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  // React to events\n  pi.on(\"session_start\", async (_event, ctx) => {\n    ctx.ui.notify(\"Extension loaded!\", \"info\");\n  });\n\n  pi.on(\"tool_call\", async (event, ctx) => {\n    if (event.toolName === \"bash\" && event.input.command?.includes(\"rm -rf\")) {\n      const ok = await ctx.ui.confirm(\"Dangerous!\", \"Allow rm -rf?\");\n      if (!ok) return { block: true, reason: \"Blocked by user\" };\n    }\n  });\n\n  // Register a custom tool\n  pi.registerTool({\n    name: \"greet\",\n    label: \"Greet\",\n    description: \"Greet someone by name\",\n    parameters: Type.Object({\n      name: Type.String({ description: \"Name to greet\" }),\n    }),\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      return {\n        content: [{ type: \"text\", text: `Hello, ${params.name}!` }],\n        details: {},\n      };\n    },\n  });\n\n  // Register a command\n  pi.registerCommand(\"hello\", {\n    description: \"Say hello\",\n    handler: async (args, ctx) => {\n      ctx.ui.notify(`Hello ${args || \"world\"}!`, \"info\");\n    },\n  });\n}\n```\n\nPruebe con la bandera `--extension` (o `-e`):\n\n```bash\npi -e ./my-extension.ts\n```\n\n## Ubicaciones de extensión\n\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.\n\nExtensions 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.\n\n| Ubicación | Alcance |\n|----------|-------|\n| `~/.pi/agent/extensions/*.ts` | Global (todos los proyectos) |\n| `~/.pi/agent/extensions/*/index.ts` | Global (subdirectorio) |\n| `.pi/extensions/*.ts` | Proyecto local |\n| `.pi/extensions/*/index.ts` | Proyecto local (subdirectorio) |\n\nRutas adicionales a través de `settings.json`:\n\n```json\n{\n  \"packages\": [\n    \"npm:@foo/bar@1.0.0\",\n    \"git:github.com/user/repo@v1\"\n  ],\n  \"extensions\": [\n    \"/path/to/local/extension.ts\",\n    \"/path/to/local/extension/dir\"\n  ]\n}\n```\n\nPara compartir extensiones a través de npm o git como paquetes pi, consulte [packages.md](packages.md).\n\n## Importaciones disponibles\n\n| Paquete | Objetivo |\n|---------|---------|\n| `@earendil-works/pi-coding-agent` | Tipos de extensión (`ExtensionAPI`, `ExtensionContext`, eventos) |\n| `typebox` | Definiciones de esquemas para parámetros de herramientas |\n| `@earendil-works/pi-ai` | Utilidades de IA (`StringEnum` para enumeraciones compatibles con Google) |\n| `@earendil-works/pi-tui` | TUI componentes para renderizado personalizado |\n\nnpm 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.\n\nPara 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.\n\nNode.js integrados (`node:fs`, `node:path`, etc.) también están disponibles.\n\n## Escribir una extensión\n\nUna extensión exporta una función predeterminada de fábrica que recibe `ExtensionAPI`. La fábrica puede ser síncrona o asíncrona:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  // Subscribe to events\n  pi.on(\"event_name\", async (event, ctx) => {\n    // ctx.ui for user interaction\n    const ok = await ctx.ui.confirm(\"Title\", \"Are you sure?\");\n    ctx.ui.notify(\"Done!\", \"info\");\n    ctx.ui.setStatus(\"my-ext\", \"Processing...\");  // Footer status\n    ctx.ui.setWidget(\"my-ext\", [\"Line 1\", \"Line 2\"]);  // Widget above editor (default)\n  });\n\n  // Register tools, commands, shortcuts, flags\n  pi.registerTool({ ... });\n  pi.registerCommand(\"name\", { ... });\n  pi.registerShortcut(\"ctrl+x\", { ... });\n  pi.registerFlag(\"my-flag\", { ... });\n}\n```\n\nExtensions se cargan a través de [jiti](https://github.com/unjs/jiti), por lo que TypeScript funciona sin compilación.\n\nSi 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()`.\n\n### Funciones de fábrica asíncronas\n\nUtilice una fábrica asíncrona para trabajos de inicio únicos, como obtener una configuración remota o descubrir dinámicamente modelos disponibles.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\nEste patrón hace que los modelos recuperados estén disponibles durante el inicio normal y hasta `pi --list-models`.\n\n### Recursos de larga duración y cierre\n\nLas 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.\n\nPosponga 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.\n\n### Estilos de extensión\n\n**Archivo único** - el más simple, para extensiones pequeñas:\n\n```\n~/.pi/agent/extensions/\n└── my-extension.ts\n```\n\n**Directorio con index.ts** - para extensiones de varios archivos:\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── index.ts        # Entry point (exports default function)\n    ├── tools.ts        # Helper module\n    └── utils.ts        # Helper module\n```\n\n**Paquete con dependencias** - para extensiones que necesitan npm paquetes:\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── package.json    # Declares dependencies and entry points\n    ├── package-lock.json\n    ├── node_modules/   # After npm install\n    └── src/\n        └── index.ts\n```\n\n```json\n// package.json\n{\n  \"name\": \"my-extension\",\n  \"dependencies\": {\n    \"zod\": \"^3.0.0\",\n    \"chalk\": \"^5.0.0\"\n  },\n  \"pi\": {\n    \"extensions\": [\"./src/index.ts\"]\n  }\n}\n```\n\nEjecute `npm install` en el directorio de extensiones, luego las importaciones desde `node_modules/` funcionan automáticamente.\n\n## Eventos\n\n### Descripción general del ciclo de vida\n\n```\npi starts\n  │\n  ├─► project_trust (user/global and CLI extensions only, before project resources load)\n  ├─► session_start { reason: \"startup\" }\n  └─► resources_discover { reason: \"startup\" }\n      │\n      ▼\nuser sends prompt ─────────────────────────────────────────┐\n  │                                                        │\n  ├─► (extension commands checked first, bypass if found)  │\n  ├─► input (can intercept, transform, or handle)          │\n  ├─► (skill/template expansion if not handled)            │\n  ├─► before_agent_start (can inject message, modify system prompt)\n  ├─► agent_start                                          │\n  ├─► message_start / message_update / message_end         │\n  │                                                        │\n  │   ┌─── turn (repeats while LLM calls tools) ───┐       │\n  │   │                                            │       │\n  │   ├─► turn_start                               │       │\n  │   ├─► context (can modify messages)            │       │\n  │   ├─► before_provider_headers (can mutate headers)     |\n  │   ├─► before_provider_request (can inspect or replace payload)\n  │   ├─► after_provider_response (status + headers, before stream consume)\n  │   │                                            │       │\n  │   │   LLM responds, may call tools:            │       │\n  │   │     ├─► tool_execution_start               │       │\n  │   │     ├─► tool_call (can block)              │       │\n  │   │     ├─► tool_execution_update              │       │\n  │   │     ├─► tool_result (can modify)           │       │\n  │   │     └─► tool_execution_end                 │       │\n  │   │                                            │       │\n  │   └─► turn_end                                 │       │\n  │                                                        │\n  ├─► agent_end                                            │\n  └─► agent_settled (no retry/compaction/follow-up left)   │\n                                                           │\nuser sends another prompt ◄────────────────────────────────┘\n\n/new (new session) or /resume (switch session)\n  ├─► session_before_switch (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"new\" | \"resume\", previousSessionFile? }\n  └─► resources_discover { reason: \"startup\" }\n\n/fork or /clone\n  ├─► session_before_fork (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"fork\", previousSessionFile }\n  └─► resources_discover { reason: \"startup\" }\n\n/name or pi.setSessionName()\n  └─► session_info_changed\n\n/compact or auto-compaction\n  ├─► session_before_compact (can cancel or customize)\n  └─► session_compact\n\n/tree navigation\n  ├─► session_before_tree (can cancel or customize)\n  └─► session_tree\n\n/model or Ctrl+P (model selection/cycling)\n  ├─► thinking_level_select (if model change changes/clamps thinking level)\n  └─► model_select\n\nthinking level changes (settings, keybinding, pi.setThinkingLevel())\n  └─► thinking_level_select\n\nexit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)\n  └─► session_shutdown\n```\n\n### Eventos de inicio\n\n#### proyecto_confianza\n\nDespedido 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.\n\n```typescript\npi.on(\"project_trust\", async (event, ctx) => {\n  // event.cwd - current working directory\n  // ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers\n  if (await ctx.ui.confirm(\"Trust project?\", event.cwd)) {\n    return { trusted: \"yes\", remember: true };\n  }\n  return { trusted: \"undecided\" };\n});\n```\n\nUn 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.\n\n### Eventos de recursos\n\n#### recursos_descubrir\n\nSe activa después de `session_start` para que las extensiones puedan contribuir con habilidades, indicaciones y rutas de temas adicionales.\nLa ruta de inicio utiliza `reason: \"startup\"`. Recargar usa `reason: \"reload\"`.\n\n```typescript\npi.on(\"resources_discover\", async (event, _ctx) => {\n  // event.cwd - current working directory\n  // event.reason - \"startup\" | \"reload\"\n  return {\n    skillPaths: [\"/path/to/skills\"],\n    promptPaths: [\"/path/to/prompts\"],\n    themePaths: [\"/path/to/themes\"],\n  };\n});\n```\n\n### Eventos de sesión\n\nConsulte [Session Format](session-format.md) para conocer los aspectos internos del almacenamiento de sesiones y el SessionManager API.\n\n#### inicio_sesión\n\nSe activa cuando se inicia, carga o recarga una sesión.\n\n```typescript\npi.on(\"session_start\", async (event, ctx) => {\n  // event.reason - \"startup\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.previousSessionFile - present for \"new\", \"resume\", and \"fork\"\n  ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? \"ephemeral\"}`, \"info\");\n});\n```\n\n#### información_sesión_cambiada\n\nSe activa cuando el nombre para mostrar de la sesión actual se establece mediante `/name`, RPC o `pi.setSessionName()`.\n\n```typescript\npi.on(\"session_info_changed\", async (event, ctx) => {\n  // event.name - current normalized name, or undefined if cleared\n  ctx.ui.notify(`Session renamed: ${event.name ?? \"(none)\"}`, \"info\");\n});\n```\n\n#### sesión_antes_de_cambio\n\nDisparado antes de iniciar una nueva sesión (`/new`) o cambiar de sesión (`/resume`).\n\n```typescript\npi.on(\"session_before_switch\", async (event, ctx) => {\n  // event.reason - \"new\" or \"resume\"\n  // event.targetSessionFile - session we're switching to (only for \"resume\")\n\n  if (event.reason === \"new\") {\n    const ok = await ctx.ui.confirm(\"Clear?\", \"Delete all messages?\");\n    if (!ok) return { cancel: true };\n  }\n});\n```\n\nDespué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`.\nRealice el trabajo de limpieza en `session_shutdown`, luego restablezca cualquier estado en memoria en `session_start`.\n\n#### sesión_antes_de_fork\n\nSe dispara al bifurcar vía `/fork` o clonar vía `/clone`.\n\n```typescript\npi.on(\"session_before_fork\", async (event, ctx) => {\n  // event.entryId - ID of the selected entry\n  // event.position - \"before\" for /fork, \"at\" for /clone\n  return { cancel: true }; // Cancel fork/clone\n  // OR\n  return { skipConversationRestore: true }; // Reserved for future conversation restore control\n});\n```\n\nDespué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`.\nRealice el trabajo de limpieza en `session_shutdown`, luego restablezca cualquier estado en memoria en `session_start`.\n\n#### sesión_antes_compact / sesión_compact\n\nCocido sobre compactación. Consulte [compaction.md](compaction.md) para obtener más detalles.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n    }\n  };\n});\n\npi.on(\"session_compact\", async (event, ctx) => {\n  // event.compactionEntry - the saved compaction\n  // event.fromExtension - whether extension provided it\n  // event.reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n});\n```\n\n#### sesión_antes_árbol / sesión_árbol\n\nDisparado en la navegación `/tree`. Consulte [Sessions](sessions.md) para conocer los conceptos de navegación en árbol.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n  return { cancel: true };\n  // OR provide custom summary:\n  return {\n    summary: {\n      summary: \"...\",\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: {},\n    },\n  };\n});\n\npi.on(\"session_tree\", async (event, ctx) => {\n  // event.newLeafId, oldLeafId, summaryEntry, fromExtension\n});\n```\n\n#### cierre_sesión\n\nSe 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.\n\n```typescript\npi.on(\"session_shutdown\", async (event, ctx) => {\n  // event.reason - \"quit\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.targetSessionFile - destination session for session replacement flows\n  // Cleanup, save state, etc.\n});\n```\n\n### Eventos de agentes\n\n#### antes_agente_inicio\n\nSe 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.\n\n```typescript\npi.on(\"before_agent_start\", async (event, ctx) => {\n  // event.prompt - user's prompt text\n  // event.images - attached images (if any)\n  // event.systemPrompt - current chained system prompt for this handler\n  //   (includes changes from earlier before_agent_start handlers)\n  // event.systemPromptOptions - structured options used to build the system prompt\n  //   .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)\n  //   .selectedTools - tools currently active in the prompt\n  //   .toolSnippets - one-line descriptions for each tool\n  //   .promptGuidelines - custom guideline bullets\n  //   .appendSystemPrompt - text from --append-system-prompt flags\n  //   .cwd - working directory\n  //   .contextFiles - AGENTS.md files and other loaded context files\n  //   .skills - loaded skills\n\n  return {\n    // Inject a persistent message (stored in session, sent to LLM)\n    message: {\n      customType: \"my-extension\",\n      content: \"Additional context for the LLM\",\n      display: true,\n    },\n    // Replace the system prompt for this turn (chained across extensions)\n    systemPrompt: event.systemPrompt + \"\\n\\nExtra instructions for this turn...\",\n  };\n});\n```\n\nEl 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.\n\nDentro 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.\n\n#### inicio_agente / fin_agente / agente_establecido\n\n`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.\n\n```typescript\npi.on(\"agent_start\", async (_event, ctx) => {});\n\npi.on(\"agent_end\", async (event, ctx) => {\n  // event.messages - messages from this low-level run\n});\n\npi.on(\"agent_settled\", async (_event, ctx) => {\n  // ctx.isIdle() is true here unless another extension started a new run.\n});\n```\n\n#### turn_start / turn_end\n\nDisparado por cada turno (una respuesta LLM + llamadas de herramientas).\n\n```typescript\npi.on(\"turn_start\", async (event, ctx) => {\n  // event.turnIndex, event.timestamp\n});\n\npi.on(\"turn_end\", async (event, ctx) => {\n  // event.turnIndex, event.message, event.toolResults\n});\n```\n\n#### inicio_mensaje / actualización_mensaje / fin_mensaje\n\nActivado por actualizaciones del ciclo de vida de los mensajes.\n\n- `message_start` y `message_end` se activan para mensajes de usuario, asistente y resultado de herramienta.\n- `message_update` activa las actualizaciones de transmisión del asistente.\n- Los controladores `message_end` pueden devolver `{ message }` para reemplazar el mensaje finalizado. El sustituto debe seguir siendo el mismo `role`.\n\n```typescript\npi.on(\"message_start\", async (event, ctx) => {\n  // event.message\n});\n\npi.on(\"message_update\", async (event, ctx) => {\n  // event.message\n  // event.assistantMessageEvent (token-by-token stream event)\n});\n\npi.on(\"message_end\", async (event, ctx) => {\n  if (event.message.role !== \"assistant\") return;\n\n  return {\n    message: {\n      ...event.message,\n      usage: {\n        ...event.message.usage,\n        cost: {\n          ...event.message.usage.cost,\n          total: 0.123,\n        },\n      },\n    },\n  };\n});\n```\n\n#### inicio_ejecución_herramienta / actualización_ejecución_herramienta / fin_ejecución_herramienta\n\nDespedido por actualizaciones del ciclo de vida de ejecución de herramientas.\n\nEn modo de herramienta paralela:\n- `tool_execution_start` se emite en el orden de la fuente asistente durante la fase de verificación previa\n- `tool_execution_update` los eventos pueden intercalarse entre herramientas\n- `tool_execution_end` se emite en el orden de finalización de la herramienta después de finalizar cada herramienta\n- Los eventos de mensajes finales `toolResult` aún se emiten más tarde en el orden de origen del asistente.\n\n```typescript\npi.on(\"tool_execution_start\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args\n});\n\npi.on(\"tool_execution_update\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args, event.partialResult\n});\n\npi.on(\"tool_execution_end\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.result, event.isError\n});\n```\n\n#### contexto\n\nDespedido antes de cada convocatoria de LLM. Modificar mensajes de forma no destructiva. Consulte [Session Format](session-format.md) para conocer los tipos de mensajes.\n\n```typescript\npi.on(\"context\", async (event, ctx) => {\n  // event.messages - deep copy, safe to modify\n  const filtered = event.messages.filter(m => !shouldPrune(m));\n  return { messages: filtered };\n});\n```\n\n#### before_provider_headers\n\nSe activa después de ensamblar los encabezados HTTP salientes. Úselo para agregar, anular o eliminar encabezados de solicitud.\n\nLos controladores mutan `event.headers` en su lugar. Establezca una clave para una cadena para agregarla o anularla, o para `null` para eliminarla.\n\n```typescript\npi.on(\"before_provider_headers\", (event, ctx) => {\n  // Add or override — e.g. a session id for gateway tracing/attribution\n  event.headers[\"x-session-id\"] = ctx.sessionManager.getSessionId();\n\n  // Drop a tracking header pi adds for this call\n  event.headers[\"X-OpenRouter-Title\"] = null;\n});\n```\n\nSe ejecuta una vez por solicitud del proveedor; Los reintentos reutilizan los mismos encabezados en lugar de volver a disparar el anzuelo.\n\n#### solicitud_antes_del_proveedor\n\nSe 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.\n\nEste 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.\n\n```typescript\npi.on(\"before_provider_request\", (event, ctx) => {\n  console.log(JSON.stringify(event.payload, null, 2));\n\n  // Optional: replace payload\n  // return { ...event.payload, temperature: 0 };\n});\n```\n\nEsto es principalmente útil para depurar la serialización del proveedor y el comportamiento de la caché.\n\n#### respuesta_después_del_proveedor\n\nSe 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.\n\n```typescript\npi.on(\"after_provider_response\", (event, ctx) => {\n  // event.status - HTTP status code\n  // event.headers - normalized response headers\n  if (event.status === 429) {\n    console.log(\"rate limited\", event.headers[\"retry-after\"]);\n  }\n});\n```\n\nLa disponibilidad del encabezado depende del proveedor y del transporte. Providers que las respuestas HTTP abstractas no pueden exponer los encabezados.\n\n### Eventos modelo\n\n#### selección_modelo\n\nSe activa cuando el modelo cambia mediante el comando `/model`, ciclo de modelo (`Ctrl+P`) o restauración de sesión.\n\n```typescript\npi.on(\"model_select\", async (event, ctx) => {\n  // event.model - newly selected model\n  // event.previousModel - previous model (undefined if first selection)\n  // event.source - \"set\" | \"cycle\" | \"restore\"\n\n  const prev = event.previousModel\n    ? `${event.previousModel.provider}/${event.previousModel.id}`\n    : \"none\";\n  const next = `${event.model.provider}/${event.model.id}`;\n\n  ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, \"info\");\n});\n```\n\nUtilí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.\n\n#### selección_nivel_pensamiento\n\nDespedido cuando cambia el nivel de pensamiento. Esto es sólo de notificación; Los valores de retorno del controlador se ignoran.\n\n```typescript\npi.on(\"thinking_level_select\", async (event, ctx) => {\n  // event.level - newly selected thinking level\n  // event.previousLevel - previous thinking level\n\n  ctx.ui.setStatus(\"thinking\", `thinking: ${event.level}`);\n});\n```\n\nUtilice 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.\n\n### Eventos de herramientas\n\n#### llamada_herramienta\n\nSe dispara después de `tool_execution_start`, antes de que se ejecute la herramienta. **Puede bloquear.** Utilice `isToolCallEventType` para restringir y obtener entradas escritas.\n\nAntes 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.\n\nEn 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`.\n\n`event.input` es mutable. Mutéelo en su lugar para parchear los argumentos de la herramienta antes de la ejecución.\n\nGarantías de comportamiento:\n- Las mutaciones en `event.input` afectan la ejecución real de la herramienta\n- Los manejadores posteriores `tool_call` ven mutaciones realizadas por manejadores anteriores\n- No se realiza ninguna revalidación después de su mutación.\n- Valores de retorno de `tool_call` bloqueo de control a través de `{ block: true, reason?: string, terminate?: boolean }`\n- `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\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_call\", async (event, ctx) => {\n  // event.toolName - \"bash\", \"read\", \"write\", \"edit\", etc.\n  // event.toolCallId\n  // event.input - tool parameters (mutable)\n\n  // Built-in tools: no type params needed\n  if (isToolCallEventType(\"bash\", event)) {\n    // event.input is { command: string; timeout?: number }\n    event.input.command = `source ~/.profile\\n${event.input.command}`;\n\n    if (event.input.command.includes(\"rm -rf\")) {\n      return { block: true, reason: \"Dangerous command\", terminate: true };\n    }\n  }\n\n  if (isToolCallEventType(\"read\", event)) {\n    // event.input is { path: string; offset?: number; limit?: number }\n    console.log(`Reading: ${event.input.path}`);\n  }\n});\n```\n\n#### Escribir entrada de herramienta personalizada\n\nLas herramientas personalizadas deben exportar su tipo de entrada:\n\n```typescript\n// my-extension.ts\nexport type MyToolInput = Static<typeof myToolSchema>;\n```\n\nUtilice `isToolCallEventType` con parámetros de tipo explícitos:\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\nimport type { MyToolInput } from \"my-extension\";\n\npi.on(\"tool_call\", (event) => {\n  if (isToolCallEventType<\"my_tool\", MyToolInput>(\"my_tool\", event)) {\n    event.input.action;  // typed\n  }\n});\n```\n\n#### resultado_herramienta\n\nSe 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.**\n\nEn 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.\n\n`tool_result` cadena de controladores como middleware:\n- Los controladores se ejecutan en orden de carga de extensión\n- Cada controlador ve el último resultado después de los cambios anteriores del controlador\n- Los controladores pueden devolver parches parciales (`content`, `details`, `isError` o `usage`); Los campos omitidos mantienen sus valores actuales.\n\nUtilice `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.\n\n```typescript\nimport { isBashToolResult } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_result\", async (event, ctx) => {\n  // event.toolName, event.toolCallId, event.input\n  // event.content, event.details, event.isError, event.usage\n\n  if (isBashToolResult(event)) {\n    // event.details is typed as BashToolDetails\n  }\n\n  const response = await fetch(\"https://example.com/summarize\", {\n    method: \"POST\",\n    body: JSON.stringify({ content: event.content }),\n    signal: ctx.signal,\n  });\n\n  // Modify result:\n  return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };\n});\n```\n\n### Eventos de bash de usuario\n\n#### usuario_bash\n\nSe activa cuando el usuario ejecuta los comandos `!` o `!!`. **Puede interceptar.**\n\n```typescript\nimport { createLocalBashOperations } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"user_bash\", (event, ctx) => {\n  // event.command - the bash command\n  // event.excludeFromContext - true if !! prefix\n  // event.cwd - working directory\n\n  // Option 1: Provide custom operations (e.g., SSH)\n  return { operations: remoteBashOps };\n\n  // Option 2: Wrap pi's built-in local bash backend\n  const local = createLocalBashOperations();\n  return {\n    operations: {\n      exec(command, cwd, options) {\n        return local.exec(`source ~/.profile\\n${command}`, cwd, options);\n      }\n    }\n  };\n\n  // Option 3: Full replacement - return result directly\n  return { result: { output: \"...\", exitCode: 0, cancelled: false, truncated: false } };\n});\n```\n\n### Eventos de entrada\n\n#### aporte\n\nSe 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.\n\n**Orden de procesamiento:**\n1. Comandos de extensión (`/cmd`) verificados primero; si se encuentran, el controlador se ejecuta y se omite el evento de entrada\n2. `input` incendios de eventos: puede interceptar, transformar o manejar\n3. Si no se maneja: comandos de habilidad (`/skill:name`) ampliados al contenido de la habilidad\n4. Si no se maneja: prompt templates (`/template`) expandido al contenido de la plantilla\n5. Comienza el procesamiento del agente (`before_agent_start`, etc.)\n\n```typescript\npi.on(\"input\", async (event, ctx) => {\n  // event.text - raw input (before skill/template expansion)\n  // event.images - attached images, if any\n  // event.source - \"interactive\" (typed), \"rpc\" (API), or \"extension\" (via sendUserMessage)\n  // event.streamingBehavior - \"steer\" | \"followUp\" | undefined\n  //   undefined when idle, \"steer\" for mid-stream interrupts,\n  //   \"followUp\" for messages queued until the agent finishes\n\n  // Transform: rewrite input before expansion\n  if (event.text.startsWith(\"?quick \"))\n    return { action: \"transform\", text: `Respond briefly: ${event.text.slice(7)}` };\n\n  // Handle: respond without LLM (extension shows its own feedback)\n  if (event.text === \"ping\") {\n    ctx.ui.notify(\"pong\", \"info\");\n    return { action: \"handled\" };\n  }\n\n  // Route by source: skip processing for extension-injected messages\n  if (event.source === \"extension\") return { action: \"continue\" };\n\n  // Intercept skill commands before expansion\n  if (event.text.startsWith(\"/skill:\")) {\n    // Could transform, block, or let pass through\n  }\n\n  return { action: \"continue\" };  // Default: pass through to expansion\n});\n```\n\n**Resultados:**\n- `continue` - pasa sin cambios (predeterminado si el controlador no devuelve nada)\n- `transform` - modificar texto/imágenes, luego continuar con la expansión\n- `handled` - omitir al agente por completo (el primer manejador que devuelva esto gana)\n\nTransforma la cadena entre controladores. Consulte [input-transform.ts](../examples/extensions/input-transform.ts) y [input-transform-streaming.ts](../examples/extensions/input-transform-streaming.ts) para conocer el enrutamiento compatible con `streamingBehavior`.\n\n## Contexto de extensión\n\nTodos los manejadores reciben `ctx: ExtensionContext`.\n\n### ctx.ui\n\nMétodos de UI para la interacción del usuario. Consulte [Custom UI](#custom-ui) para obtener todos los detalles.\n\n### modo ctx\n\nModo 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.\n\n### ctx.hasUI\n\n`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](rpc.md#extension-ui-protocol)).\n\n### ctx.cwd\n\nDirectorio de trabajo actual.\n\nUtilice `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.\n\n```typescript\nimport { CONFIG_DIR_NAME, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { join } from \"node:path\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, \"my-extension.json\");\n    // ...\n  });\n}\n```\n\n### ctx.isProjectTrusted()\n\nDevuelve 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.\n\nUtilice esto antes de leer la configuración de la extensión local del proyecto que solo debe respetarse para proyectos confiables.\n\n### ctx.sessionManager\n\nAcceso de solo lectura al estado de la sesión. Consulte [Session Format](session-format.md) para conocer el SessionManager completo API y los tipos de entrada.\n\nPara `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.\n\n```typescript\nctx.sessionManager.getEntries()             // All entries\nctx.sessionManager.getBranch()              // Current branch\nctx.sessionManager.buildContextEntries()    // Active branch entries with compaction applied\nctx.sessionManager.getLeafId()              // Current leaf entry ID\n```\n\n### ctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels\n\nAcceso 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.\n\n`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()`.\n\n### señal.ctx\n\nLa señal de cancelación del agente actual, o `undefined` cuando no hay ningún turno de agente activo.\n\nÚselo para trabajos anidados con detección de abortos iniciados por controladores de extensiones, por ejemplo:\n- `fetch(..., { signal: ctx.signal })`\n- llamadas modelo que aceptan `signal`\n- archivar o procesar ayudantes que acepten `AbortSignal`\n\n`ctx.signal` normalmente se define durante eventos de turnos activos como `tool_call`, `tool_result`, `message_update` y `turn_end`.\nPor 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.\n\n```typescript\npi.on(\"tool_result\", async (event, ctx) => {\n  const response = await fetch(\"https://example.com/api\", {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    signal: ctx.signal,\n  });\n\n  const data = await response.json();\n  return { details: data };\n});\n```\n\n### ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()\n\nControle 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.\n\n### ctx.apagado()\n\nSolicite un cierre elegante de pi.\n\n- **Modo interactivo:** Diferido hasta que el agente esté inactivo (después de procesar todos los mensajes de dirección y seguimiento en cola).\n- **RPC modo:** Diferido hasta el siguiente estado inactivo (después de completar la respuesta del comando actual, mientras se espera el siguiente comando).\n- **Modo de impresión:** No operativo. El proceso sale automáticamente cuando se procesan todas las solicitudes.\n\nEmite el evento `session_shutdown` a todas las extensiones antes de salir. Disponible en todos los contextos (controladores de eventos, herramientas, comandos, accesos directos).\n\n```typescript\npi.on(\"tool_call\", (event, ctx) => {\n  if (isFatal(event.input)) {\n    ctx.shutdown();\n  }\n});\n```\n\n### ctx.getContextUsage()\n\nDevuelve 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.\n\n```typescript\nconst usage = ctx.getContextUsage();\nif (usage && usage.tokens > 100_000) {\n  // ...\n}\n```\n\n### ctx.compacto()\n\nActivar la compactación sin esperar a que finalice. Utilice `onComplete` y `onError` para acciones de seguimiento.\n\n```typescript\nctx.compact({\n  customInstructions: \"Focus on recent changes\",\n  onComplete: (result) => {\n    ctx.ui.notify(\"Compaction completed\", \"info\");\n  },\n  onError: (error) => {\n    ctx.ui.notify(`Compaction failed: ${error.message}`, \"error\");\n  },\n});\n```\n\n### ctx.getSystemPrompt()\n\nDevuelve la cadena de aviso del sistema actual de Pi.\n\n- Durante `before_agent_start`, esto refleja los cambios encadenados de indicaciones del sistema realizados hasta el momento para el turno actual.\n- No incluye mutaciones de mensajes `context` posteriores.\n- No incluye reescrituras de carga útil `before_provider_request`.\n- Si las extensiones cargadas posteriormente se ejecutan después de la suya, aún pueden cambiar lo que se envía finalmente.\n\n```typescript\npi.on(\"before_agent_start\", (event, ctx) => {\n  const prompt = ctx.getSystemPrompt();\n  console.log(`System prompt length: ${prompt.length}`);\n});\n```\n\n## Contexto de comando de extensión\n\nLos 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.\n\n### ctx.getSystemPromptOptions()\n\nDevuelve las entradas base que Pi utiliza actualmente para crear el indicador del sistema.\n\n```typescript\nconst options = ctx.getSystemPromptOptions();\nconst contextPaths = options.contextFiles?.map((file) => file.path) ?? [];\n```\n\nTiene 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.\n\nEsto 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`.\n\n### ctx.waitForIdle()\n\nEspere 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:\n\n```typescript\npi.registerCommand(\"my-cmd\", {\n  handler: async (args, ctx) => {\n    await ctx.waitForIdle();\n    // Agent is now idle, safe to modify session\n  },\n});\n```\n\n### ctx.newSession(¿opciones?)\n\nCrea una nueva sesión:\n\n```typescript\nconst parentSession = ctx.sessionManager.getSessionFile();\nconst kickoff = \"Continue in the replacement session\";\n\nconst result = await ctx.newSession({\n  parentSession,\n  setup: async (sm) => {\n    sm.appendMessage({\n      role: \"user\",\n      content: [{ type: \"text\", text: \"Context from previous session...\" }],\n      timestamp: Date.now(),\n    });\n  },\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    await ctx.sendUserMessage(kickoff);\n  },\n});\n\nif (result.cancelled) {\n  // An extension cancelled the new session\n}\n```\n\nOpciones:\n- `parentSession`: archivo de sesión principal para registrar en el encabezado de la nueva sesión\n- `setup`: muta el `SessionManager` de la nueva sesión antes de que se ejecute `withSession`\n- `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](#session-replacement-lifecycle-and-footguns).\n\n### ctx.fork(entryId, ¿opciones?)\n\nBifurca desde una entrada específica, creando un nuevo archivo de sesión:\n\n```typescript\nconst result = await ctx.fork(\"entry-id-123\", {\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    ctx.ui.notify(\"Now in the forked session\", \"info\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the fork\n}\n\nconst cloneResult = await ctx.fork(\"entry-id-456\", { position: \"at\" });\nif (cloneResult.cancelled) {\n  // An extension cancelled the clone\n}\n```\n\nOpciones:\n- `position`: `\"before\"` (predeterminado) se bifurca antes del mensaje del usuario seleccionado, restaurando ese mensaje en el editor\n- `position`: `\"at\"` duplica la ruta activa a través de la entrada seleccionada sin restaurar el texto del editor\n- `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](#session-replacement-lifecycle-and-footguns).\n\n### ctx.navigateTree(ID de destino, ¿opciones?)\n\nNavega a un punto diferente en el session tree:\n\n```typescript\nconst result = await ctx.navigateTree(\"entry-id-456\", {\n  summarize: true,\n  customInstructions: \"Focus on error handling changes\",\n  replaceInstructions: false, // true = replace default prompt entirely\n  label: \"review-checkpoint\",\n});\n```\n\nOpciones:\n- `summarize`: Si se debe generar un resumen de la rama abandonada\n- `customInstructions`: Instrucciones personalizadas para el resumidor\n- `replaceInstructions`: si es verdadero, `customInstructions` reemplaza el mensaje predeterminado en lugar de agregarlo\n- `label`: Etiqueta para adjuntar a la entrada de resumen de la rama (o entrada de destino si no es un resumen)\n\n### ctx.switchSession (ruta de sesión, ¿opciones?)\n\nCambie a un archivo de sesión diferente:\n\n```typescript\nconst result = await ctx.switchSession(\"/path/to/session.jsonl\", {\n  withSession: async (ctx) => {\n    await ctx.sendUserMessage(\"Resume work in the replacement session\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the switch via session_before_switch\n}\n```\n\nOpciones:\n- `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](#session-replacement-lifecycle-and-footguns).\n\nPara descubrir sesiones disponibles, utilice los métodos estáticos `SessionManager.list()` o `SessionManager.listAll()`:\n\n```typescript\nimport { SessionManager } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"switch\", {\n  description: \"Switch to another session\",\n  handler: async (args, ctx) => {\n    const sessions = await SessionManager.list(ctx.cwd);\n    if (sessions.length === 0) return;\n    const choice = await ctx.ui.select(\n      \"Pick session:\",\n      sessions.map(s => s.file),\n    );\n    if (choice) {\n      await ctx.switchSession(choice, {\n        withSession: async (ctx) => {\n          ctx.ui.notify(\"Switched session\", \"info\");\n        },\n      });\n    }\n  },\n});\n```\n\n### Ciclo de vida de reemplazo de sesión y pistolas\n\n`withSession` recibe un `ReplacedSessionContext` nuevo, que extiende `ExtensionCommandContext` con ayudantes asíncronos `sendMessage()` y `sendUserMessage()` vinculados a la sesión de reemplazo.\n\nCiclo de vida y pistolas:\n- `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`.\n- 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`.\n- 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.\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.\n- 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.\n\nPatrón seguro:\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const kickoff = \"Continue from the replacement session\";\n    await ctx.newSession({\n      withSession: async (ctx) => {\n        await ctx.sendUserMessage(kickoff);\n      },\n    });\n  },\n});\n```\n\nPatrón inseguro:\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const oldSessionManager = ctx.sessionManager;\n    await ctx.newSession({\n      withSession: async (_ctx) => {\n        // stale old objects: do not do this\n        oldSessionManager.getSessionFile();\n        pi.sendUserMessage(\"wrong\");\n      },\n    });\n  },\n});\n```\n\n### ctx.recargar()\n\nEjecute el mismo flujo de recarga que `/reload`.\n\n```typescript\npi.registerCommand(\"reload-runtime\", {\n  description: \"Reload extensions, skills, prompts, themes, and context files\",\n  handler: async (_args, ctx) => {\n    await ctx.reload();\n    return;\n  },\n});\n```\n\nComportamiento importante:\n- `await ctx.reload()` emite `session_shutdown` para el tiempo de ejecución de la extensión actual\n- Luego recarga recursos y emite `session_start` con `reason: \"reload\"` y `resources_discover` con motivo `\"reload\"`\n- El controlador de comandos que se está ejecutando actualmente aún continúa en el marco de llamada anterior.\n- El código después de `await ctx.reload()` todavía se ejecuta desde la versión previa a la recarga\n- 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\n- Después de que el controlador regrese, los comandos/eventos/llamadas a herramientas futuros usarán la nueva versión de la extensión.\n\nPara un comportamiento predecible, trate la recarga como terminal para ese controlador (`await ctx.reload(); return;`).\n\nLas 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.\n\nHerramienta de ejemplo que el LLM puede llamar para activar la recarga:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"reload-runtime\", {\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    handler: async (_args, ctx) => {\n      await ctx.reload();\n      return;\n    },\n  });\n\n  pi.registerTool({\n    name: \"reload_runtime\",\n    label: \"Reload Runtime\",\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    parameters: Type.Object({}),\n    async execute() {\n      pi.sendUserMessage(\"/reload-runtime\", { deliverAs: \"followUp\" });\n      return {\n        content: [{ type: \"text\", text: \"Queued /reload-runtime as a follow-up command.\" }],\n      };\n    },\n  });\n}\n```\n\n## ExtensiónAPI Métodos\n\n### pi.on(evento, controlador)\n\nSuscríbete a eventos. Consulte [Events](#events) para conocer los tipos de eventos y los valores de retorno.\n\n### pi.registerTool(definición)\n\nRegistre una herramienta personalizada a la que pueda llamar el LLM. Consulte [Custom Tools](#custom-tools) para obtener todos los detalles.\n\n`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`.\n\nUtilice `pi.setActiveTools()` para habilitar o deshabilitar herramientas (incluidas las herramientas agregadas dinámicamente) en tiempo de ejecución.\n\nUtilice `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.\n\n**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...\".\n\nConsulte [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts) para ver un ejemplo completo.\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does\",\n  promptSnippet: \"Summarize or transform text according to action\",\n  promptGuidelines: [\"Use my_tool when the user asks to summarize previously generated text.\"],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    // Optional compatibility shim. Runs before schema validation.\n    // Return the current schema shape, for example to fold legacy fields\n    // into the modern parameter object.\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Stream progress\n    onUpdate?.({ content: [{ type: \"text\", text: \"Working...\" }] });\n\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],\n      details: { result: \"...\" },\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n### pi.sendMessage(mensaje, ¿opciones?)\n\nInyecte 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()`](#piappendentrycustomtype-data) con [`pi.registerEntryRenderer()`](#piregisterentryrenderercustomtype-renderer).\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",\n  content: \"Message text\",\n  display: true,\n  details: { ... },\n}, {\n  triggerTurn: true,\n  deliverAs: \"steer\",\n});\n```\n\n**Opciones:**\n- `deliverAs` - Modo de entrega:\n  - `\"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.\n  - `\"followUp\"`: espera a que termine el agente. Se entrega solo cuando el agente no tiene más llamadas de herramientas.\n  - `\"nextTurn\"`: en cola para el siguiente mensaje de usuario. No interrumpe ni desencadena nada.\n- `triggerTurn: true`: si el agente está inactivo, activa una respuesta LLM inmediatamente. Solo se aplica a los modos `\"steer\"` y `\"followUp\"` (ignorado para `\"nextTurn\"`).\n\n### pi.sendUserMessage(contenido, opciones?)\n\nEnviar 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.\n\n```typescript\n// Simple text message\npi.sendUserMessage(\"What is 2+2?\");\n\n// With content array (text + images)\npi.sendUserMessage([\n  { type: \"text\", text: \"Describe this image:\" },\n  { type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } },\n]);\n\n// During streaming - must specify delivery mode\npi.sendUserMessage(\"Focus on error handling\", { deliverAs: \"steer\" });\npi.sendUserMessage(\"And then summarize\", { deliverAs: \"followUp\" });\n```\n\n**Opciones:**\n- `deliverAs` - Requerido cuando el agente está transmitiendo:\n  - `\"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.\n  - `\"followUp\"`: espera a que el agente termine todas las herramientas\n\nCuando no se transmite, el mensaje se envía inmediatamente y desencadena un nuevo turno. Cuando se transmite sin `deliverAs`, se genera un error.\n\nConsulte [send-user-message.ts](../examples/extensions/send-user-message.ts) para ver un ejemplo completo.\n\n### pi.appendEntry(tipo personalizado, ¿datos?)\n\nPersistir 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()`.\n\n```typescript\npi.appendEntry(\"my-state\", { count: 42 });\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n\n// Restore on reload\npi.on(\"session_start\", async (_event, ctx) => {\n  for (const entry of ctx.sessionManager.getEntries()) {\n    if (entry.type === \"custom\" && entry.customType === \"my-state\") {\n      // Reconstruct from entry.data\n    }\n  }\n});\n```\n\n### pi.setSessionName(nombre)\n\nEstablezca el nombre para mostrar de la sesión (que se muestra en el selector de sesión en lugar del primer mensaje).\n\n```typescript\npi.setSessionName(\"Refactor auth module\");\n```\n\n### pi.getNombreSesión()\n\nObtenga el nombre de la sesión actual, si está configurado.\n\n```typescript\nconst name = pi.getSessionName();\nif (name) {\n  console.log(`Session: ${name}`);\n}\n```\n\n### pi.setLabel(entryId, etiqueta)\n\nEstablecer 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`).\n\n```typescript\n// Set a label\npi.setLabel(entryId, \"checkpoint-before-refactor\");\n\n// Clear a label\npi.setLabel(entryId, undefined);\n\n// Read labels via sessionManager\nconst label = ctx.sessionManager.getLabel(entryId);\n```\n\nLas 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.\n\n### pi.registerCommand(nombre, opciones)\n\nRegistre un comando.\n\nSi 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`.\n\n```typescript\npi.registerCommand(\"stats\", {\n  description: \"Show session statistics\",\n  handler: async (args, ctx) => {\n    const count = ctx.sessionManager.getEntries().length;\n    ctx.ui.notify(`${count} entries`, \"info\");\n  }\n});\n```\n\nOpcional: agregue el argumento de autocompletado para `/command...`:\n\n```typescript\nimport type { AutocompleteItem } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"deploy\", {\n  description: \"Deploy to an environment\",\n  getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {\n    const envs = [\"dev\", \"staging\", \"prod\"];\n    const items = envs.map((e) => ({ value: e, label: e }));\n    const filtered = items.filter((i) => i.value.startsWith(prefix));\n    return filtered.length > 0 ? filtered : null;\n  },\n  handler: async (args, ctx) => {\n    ctx.ui.notify(`Deploying: ${args}`, \"info\");\n  },\n});\n```\n\n### pi.getCommands()\n\nObtenga 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.\nLa lista coincide con el orden RPC `get_commands`: primero las extensiones, luego las plantillas y luego las habilidades.\n\n```typescript\nconst commands = pi.getCommands();\nconst bySource = commands.filter((command) => command.source === \"extension\");\nconst userScoped = commands.filter((command) => command.sourceInfo.scope === \"user\");\n```\n\nCada entrada tiene esta forma:\n\n```typescript\n{\n  name: string; // Invokable command name without the leading slash. May be suffixed like \"review:1\"\n  description?: string;\n  source: \"extension\" | \"prompt\" | \"skill\";\n  sourceInfo: {\n    path: string;\n    source: string;\n    scope: \"user\" | \"project\" | \"temporary\";\n    origin: \"package\" | \"top-level\";\n    baseDir?: string;\n  };\n}\n```\n\nUtilice `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.\n\nLos comandos interactivos integrados (como `/model` y `/settings`) no se incluyen aquí. Se manejan sólo en interactivo.\nmodo y no se ejecutaría si se enviara a través de `prompt`.\n\n### pi.registerMessageRenderer (tipo personalizado, renderizador)\n\nRegistre 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](#custom-ui).\n\n### pi.registerMarkdownTransformer(transformador)\n\nRegistre 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.\n\nEl transformador recibe la cadena Markdown y un contexto con:\n\n- `messageType` — `\"user\"`, `\"assistant\"` o `\"assistant-thinking\"`\n- `isStreaming` — `true` para actualizaciones parciales del asistente; `false` para usuario, asistente finalizado y mensajes restaurados\n- `availableWidth`: columnas terminales exactas disponibles para el contenido Markdown transformado\n\nDevuelve el transformado Markdown:\n\n```typescript\npi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {\n  if (isStreaming || messageType === \"assistant-thinking\") return markdown;\n  return markdown.replaceAll(\"-->\", \"→\");\n});\n```\n\nSi 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.\n\n### pi.registerEntryRenderer (tipo personalizado, renderizador)\n\nRegistre 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.\n\n```typescript\nimport { Box, Text } from \"@earendil-works/pi-tui\";\n\npi.registerEntryRenderer(\"status-card\", (entry, { expanded }, theme) => {\n  const data = entry.data as { title: string; count: number };\n  const box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n  box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));\n  if (expanded) {\n    box.addChild(new Text(theme.fg(\"dim\", JSON.stringify(data, null, 2))));\n  }\n  return box;\n});\n\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n```\n\n### pi.registerShortcut(atajo, opciones)\n\nRegistre un atajo de teclado. Consulte [keybindings.md](keybindings.md) para conocer el formato de acceso directo y las combinaciones de teclas integradas.\n\n```typescript\npi.registerShortcut(\"ctrl+shift+p\", {\n  description: \"Toggle plan mode\",\n  handler: async (ctx) => {\n    ctx.ui.notify(\"Toggled!\");\n  },\n});\n```\n\n### pi.registerFlag(nombre, opciones)\n\nRegistre una bandera CLI.\n\n```typescript\npi.registerFlag(\"plan\", {\n  description: \"Start in plan mode\",\n  type: \"boolean\",\n  default: false,\n});\n\n// Check value\nif (pi.getFlag(\"plan\")) {\n  // Plan mode enabled\n}\n```\n\n### pi.exec (comando, argumentos, opciones?)\n\nEjecute un comando de shell.\n\n```typescript\nconst result = await pi.exec(\"git\", [\"status\"], { signal, timeout: 5000 });\n// result.stdout, result.stderr, result.code, result.killed\n```\n\n### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(nombres)\n\nAdministrar 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.\n\n```typescript\nconst active = pi.getActiveTools(); // [\"read\", \"bash\", ...]\nconst all = pi.getAllTools();\n// all = [{\n//   name: \"read\",\n//   description: \"Read file contents...\",\n//   parameters: ...,\n//   promptGuidelines: [\"Use read to examine files instead of cat or sed.\"],\n//   sourceInfo: { path: \"<builtin:read>\", source: \"builtin\", scope: \"temporary\", origin: \"top-level\" }\n// }, ...]\nconst builtinTools = all.filter((t) => t.sourceInfo.source === \"builtin\");\nconst extensionTools = all.filter((t) => t.sourceInfo.source !== \"builtin\" && t.sourceInfo.source !== \"sdk\");\npi.setActiveTools([...new Set([...active, \"my_custom_tool\"])]); // Keep current tools and enable my_custom_tool\npi.setActiveTools([\"read\", \"bash\"]); // Switch to read-only\n```\n\n`pi.getAllTools()` devuelve `name`, `description`, `parameters`, `promptGuidelines` y `sourceInfo`.\n\nValores típicos de `sourceInfo.source`:\n- `builtin` para herramientas integradas\n- `sdk` para herramientas pasadas por `createAgentSession({ customTools })`\n- Metadatos de origen de extensión para herramientas registradas por extensiones.\n\n### pi.setModel(modelo)\n\nEstablecer el modelo actual. Devuelve `false` si no hay API key disponible para el modelo. Consulte [models.md](models.md) para configurar modelos personalizados.\n\n```typescript\nconst model = ctx.modelRegistry.find(\"anthropic\", \"claude-sonnet-4-5\");\nif (model) {\n  const success = await pi.setModel(model);\n  if (!success) {\n    ctx.ui.notify(\"No API key for this model\", \"error\");\n  }\n}\n```\n\n### pi.getThinkingLevel() / pi.setThinkingLevel(nivel)\n\nObtenga 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`.\n\n```typescript\nconst current = pi.getThinkingLevel();  // \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\"\npi.setThinkingLevel(\"high\");\n```\n\n### pi.eventos\n\nBus de eventos compartido para comunicación entre extensiones:\n\n```typescript\npi.events.on(\"my:event\", (data) => { ... });\npi.events.emit(\"my:event\", { ... });\n```\n\n### pi.registerProvider(nombre, configuración)\n\nRegistre o anule un proveedor de modelo dinámicamente. Útil para servidores proxy, puntos finales personalizados o configuraciones de modelos para todo el equipo.\n\nLas 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`.\n\nLos 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.\n\n`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.\n\nExtensions 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.\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\n\nconst provider = createProvider({\n  id: \"local-server\",\n  name: \"Local Server\",\n  baseUrl: \"http://localhost:8080/v1\",\n  auth: {\n    apiKey: {\n      name: \"Local server setup\",\n      async login(interaction) {\n        return {\n          type: \"api_key\",\n          key: await interaction.prompt({ type: \"secret\", message: \"API key\" }),\n        };\n      },\n      async resolve({ credential }) {\n        return credential?.key\n          ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n          : undefined;\n      },\n    },\n  },\n  models: [],\n  api: openAICompletionsApi(),\n});\n\npi.registerProvider(provider);\n\n// Register a new provider with custom models\npi.registerProvider(\"my-proxy\", {\n  name: \"My Proxy\",\n  baseUrl: \"https://proxy.example.com\",\n  apiKey: \"$PROXY_API_KEY\",  // env var reference\n  api: \"anthropic-messages\",\n  models: [\n    {\n      id: \"claude-sonnet-4-20250514\",\n      name: \"Claude 4 Sonnet (proxy)\",\n      reasoning: false,\n      input: [\"text\", \"image\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Register a live llama.cpp catalog without persisting discovered models\npi.registerProvider(\"llama.cpp\", {\n  baseUrl: \"http://localhost:8080/v1\",\n  apiKey: \"local\",\n  api: \"openai-completions\",\n  async refreshModels({ signal }) {\n    const response = await fetch(\"http://localhost:8080/v1/models\", { signal });\n    const { data } = await response.json();\n    return data.map(({ id }) => ({\n      id,\n      name: id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 128000,\n      maxTokens: 16384\n    }));\n  }\n});\n\n// Override baseUrl for an existing provider (keeps all models)\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Register provider with OAuth support for /login\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n    async login(callbacks) {\n      // Custom OAuth flow\n      callbacks.onAuth({ url: \"https://sso.corp.com/...\" });\n      const code = await callbacks.onPrompt({ message: \"Enter code:\" });\n      return { refresh: code, access: code, expires: Date.now() + 3600000 };\n    },\n    async refreshToken(credentials, signal) {\n      signal.throwIfAborted();\n      // Refresh logic\n      return credentials;\n    },\n    getApiKey(credentials) {\n      return credentials.access;\n    }\n  }\n});\n```\n\nLa forma del objeto acepta un pi-ai completo `Provider`, incluido el comportamiento nativo `auth`, `getModels`, `refreshModels`, `filterModels`, `stream` y `streamSimple`.\n\n**Opciones de configuración heredadas:**\n- `name`: nombre para mostrar del proveedor en la interfaz de usuario, como `/login`.\n- `baseUrl` - API URL del punto final. Requerido al definir modelos.\n- `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_VAR` o `${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.\n- `api` - API tipo: `\"anthropic-messages\"`, `\"openai-completions\"`, `\"openai-responses\"`, etc.\n- `headers`: encabezados personalizados para incluir en las solicitudes.\n- `authHeader`: si es verdadero, agrega el encabezado `Authorization: Bearer` automáticamente.\n- `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.\n- `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.\n- `oauth` - OAuth configuración del proveedor para soporte `/login`. Cuando se proporciona, el proveedor aparece en el menú de inicio de sesión.\n- `streamSimple`: implementación de transmisión personalizada para API no estándar.\n\nConsulte [custom-provider.md](custom-provider.md) para temas avanzados: transmisión personalizada APIs, detalles OAuth, referencia de definición de modelo.\n\n### pi.unregisterProvider(nombre)\n\nEliminar 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.\n\nAl 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`.\n\n```typescript\npi.registerCommand(\"my-setup-teardown\", {\n  description: \"Remove the custom proxy provider\",\n  handler: async (_args, _ctx) => {\n    pi.unregisterProvider(\"my-proxy\");\n  },\n});\n```\n\n## Gestión del Estado\n\nExtensions con estado debería almacenarlo en el resultado de la herramienta `details` para un soporte de ramificación adecuado:\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let items: string[] = [];\n\n  // Reconstruct state from session\n  pi.on(\"session_start\", async (_event, ctx) => {\n    items = [];\n    for (const entry of ctx.sessionManager.getBranch()) {\n      if (entry.type === \"message\" && entry.message.role === \"toolResult\") {\n        if (entry.message.toolName === \"my_tool\") {\n          items = entry.message.details?.items ?? [];\n        }\n      }\n    }\n  });\n\n  pi.registerTool({\n    name: \"my_tool\",\n    // ...\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      items.push(\"new item\");\n      return {\n        content: [{ type: \"text\", text: \"Added\" }],\n        details: { items: [...items] },  // Store for reconstruction\n      };\n    },\n  });\n}\n```\n\n## Herramientas personalizadas\n\nRegistre 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.\n\nUtilice `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.\n\nUtilice `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([...])`).\n\n**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...\".\n\nNota: 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.\n\nSi 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.\n\nEjemplo 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.\n\nPase 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()`.\n\nPonga 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.\n\n```typescript\nimport { withFileMutationQueue } from \"@earendil-works/pi-coding-agent\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\n\nasync execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n  const absolutePath = resolve(ctx.cwd, params.path);\n\n  return withFileMutationQueue(absolutePath, async () => {\n    await mkdir(dirname(absolutePath), { recursive: true });\n    const current = await readFile(absolutePath, \"utf8\");\n    const next = current.replace(params.oldText, params.newText);\n    await writeFile(absolutePath, next, \"utf8\");\n\n    return {\n      content: [{ type: \"text\", text: `Updated ${params.path}` }],\n      details: {},\n    };\n  });\n}\n```\n\n### Definición de herramienta\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does (shown to LLM)\",\n  promptSnippet: \"List or add items in the project todo list\",\n  promptGuidelines: [\n    \"Use my_tool for todo planning instead of direct file edits when the user asks for a task list.\"\n  ],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),  // Use StringEnum for Google compatibility\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n    const input = args as { action?: string; oldAction?: string };\n    if (typeof input.oldAction === \"string\" && input.action === undefined) {\n      return { ...input, action: input.oldAction };\n    }\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Check for cancellation\n    if (signal?.aborted) {\n      return { content: [{ type: \"text\", text: \"Cancelled\" }] };\n    }\n\n    // Stream progress updates\n    onUpdate?.({\n      content: [{ type: \"text\", text: \"Working...\" }],\n      details: { progress: 50 },\n    });\n\n    // Run commands via pi.exec (captured from extension closure)\n    const result = await pi.exec(\"some-command\", [], { signal });\n\n    // Return result\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],  // Sent to LLM\n      details: { data: result },                   // For rendering & state\n      // usage: nestedModelResponse.usage,          // Optional nested LLM usage\n      // Optional: stop after this tool batch when every finalized tool result\n      // in the batch also returns terminate: true.\n      terminate: true,\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n**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.\n\n**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.\n\n**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](../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.\n\n```typescript\n// Correct: throw to signal an error\nasync execute(toolCallId, params) {\n  if (!isValid(params.input)) {\n    throw new Error(`Invalid input: ${params.input}`);\n  }\n  return { content: [{ type: \"text\", text: \"OK\" }], details: {} };\n}\n```\n\n**Importante:** Utilice `StringEnum` desde `@earendil-works/pi-ai` para enumeraciones de cadenas. `Type.Union`/`Type.Literal` no funciona con el API de Google.\n\n**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.\n\nEjemplo: 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 }]`.\n\n```typescript\npi.registerTool({\n  name: \"edit\",\n  label: \"Edit\",\n  description: \"Edit a single file using exact text replacement\",\n  parameters: Type.Object({\n    path: Type.String(),\n    edits: Type.Array(\n      Type.Object({\n        oldText: Type.String(),\n        newText: Type.String(),\n      }),\n    ),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n\n    const input = args as {\n      path?: string;\n      edits?: Array<{ oldText: string; newText: string }>;\n      oldText?: unknown;\n      newText?: unknown;\n    };\n\n    if (typeof input.oldText !== \"string\" || typeof input.newText !== \"string\") {\n      return args;\n    }\n\n    return {\n      ...input,\n      edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],\n    };\n  },\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // params now matches the current schema\n    return {\n      content: [{ type: \"text\", text: `Applying ${params.edits.length} edit block(s)` }],\n      details: {},\n    };\n  },\n});\n```\n\n### Anulación de herramientas integradas\n\nExtensions 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.\n\n```bash\n# Extension's read tool replaces built-in read\npi -e ./tool-override.ts\n```\n\nAlternativamente, use `--no-builtin-tools` para comenzar sin herramientas integradas mientras mantiene habilitadas las herramientas de extensión:\n```bash\n# No built-in tools, only extension tools\npi --no-builtin-tools -e ./my-extension.ts\n```\n\nConsulte [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts) para ver un ejemplo completo que anula `read` con el registro y el control de acceso.\n\n**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.\n\n**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.\n\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.\n\nImplementaciones de herramientas integradas:\n- [read.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`\n- [bash.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`\n- [edit.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)\n- [write.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)\n- [grep.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`\n- [find.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`\n- [ls.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`\n\n### Ejecución remota\n\nLas herramientas integradas admiten operaciones conectables para delegar a sistemas remotos (SSH, contenedores, etc.):\n\n```typescript\nimport { createReadTool, createBashTool, type ReadOperations } from \"@earendil-works/pi-coding-agent\";\n\n// Create tool with custom operations\nconst remoteRead = createReadTool(cwd, {\n  operations: {\n    readFile: (path) => sshExec(remote, `cat ${path}`),\n    access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),\n  }\n});\n\n// Register, checking flag at execution time\npi.registerTool({\n  ...remoteRead,\n  async execute(id, params, signal, onUpdate, _ctx) {\n    const ssh = getSshConfig();\n    if (ssh) {\n      const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });\n      return tool.execute(id, params, signal, onUpdate);\n    }\n    return localRead.execute(id, params, signal, onUpdate);\n  },\n});\n```\n\n**Interfaces de operaciones:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`\n\nPara `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.\n\nLa herramienta bash también admite un gancho de generación para ajustar el comando, cwd o env antes de la ejecución:\n\n```typescript\nimport { createBashTool } from \"@earendil-works/pi-coding-agent\";\n\nconst bashTool = createBashTool(cwd, {\n  spawnHook: ({ command, cwd, env }) => ({\n    command: `source ~/.profile\\n${command}`,\n    cwd: `/mnt/sandbox${cwd}`,\n    env: { ...env, CI: \"1\" },\n  }),\n});\n```\n\n`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:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n});\n```\n\nConsulte [Bash tool session environment](environment-variables.md#bash-tool-session-environment) para conocer la semántica de las variables. Consulte [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) para ver un ejemplo completo de SSH con la bandera `--ssh`.\n\n### Truncamiento de salida\n\n**Las herramientas DEBEN truncar su salida** para evitar abrumar el contexto LLM. Grandes producciones pueden causar:\n- Errores de desbordamiento de contexto (mensaje demasiado largo)\n- Fallas de compactación\n- Rendimiento del modelo degradado\n\nEl límite incorporado es **50 KB** (~10 000 tokens) y **2000 líneas**, lo que se alcance primero. Utilice las utilidades de truncamiento exportadas:\n\n```typescript\nimport {\n  truncateHead,      // Keep first N lines/bytes (good for file reads, search results)\n  truncateTail,      // Keep last N lines/bytes (good for logs, command output)\n  truncateLine,      // Truncate a single line to maxBytes with ellipsis\n  formatSize,        // Human-readable size (e.g., \"50KB\", \"1.5MB\")\n  DEFAULT_MAX_BYTES, // 50KB\n  DEFAULT_MAX_LINES, // 2000\n} from \"@earendil-works/pi-coding-agent\";\n\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const output = await runCommand();\n\n  // Apply truncation\n  const truncation = truncateHead(output, {\n    maxLines: DEFAULT_MAX_LINES,\n    maxBytes: DEFAULT_MAX_BYTES,\n  });\n\n  let result = truncation.content;\n\n  if (truncation.truncated) {\n    // Write full output to temp file\n    const tempFile = writeTempFile(output);\n\n    // Inform the LLM where to find complete output\n    result += `\\n\\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;\n    result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;\n    result += ` Full output saved to: ${tempFile}]`;\n  }\n\n  return { content: [{ type: \"text\", text: result }] };\n}\n```\n\n**Puntos clave:**\n- Utilice `truncateHead` para contenido donde el comienzo importa (resultados de búsqueda, lecturas de archivos)\n- Utilice `truncateTail` para contenido donde el final importa (registros, salida de comando)\n- Informe siempre al LLM cuando se trunque el resultado y dónde encontrar la versión completa\n- Documente los límites de truncamiento en la descripción de su herramienta.\n\nConsulte [examples/extensions/truncated-tool.ts](../examples/extensions/truncated-tool.ts) para ver un ejemplo completo de ajuste de `rg` (ripgrep) con el truncamiento adecuado.\n\n### Múltiples herramientas\n\nUna extensión puede registrar múltiples herramientas con estado compartido:\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let connection = null;\n\n  pi.registerTool({ name: \"db_connect\", ... });\n  pi.registerTool({ name: \"db_query\", ... });\n  pi.registerTool({ name: \"db_close\", ... });\n\n  pi.on(\"session_shutdown\", async () => {\n    connection?.close();\n  });\n}\n```\n\n### Representación personalizada\n\nLas herramientas pueden proporcionar `renderCall` y `renderResult` para una visualización personalizada de TUI. Consulte [tui.md](tui.md) para conocer el componente completo API y [tool-execution.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) para conocer cómo se componen las filas de herramientas.\n\nDe 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.\n\nEstablezca `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.\n\n```typescript\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Custom shell example\",\n  parameters: Type.Object({}),\n  renderShell: \"self\",\n  async execute() {\n    return { content: [{ type: \"text\", text: \"ok\" }], details: undefined };\n  },\n  renderCall(args, theme, context) {\n    return new Text(theme.fg(\"accent\", \"my custom shell\"), 0, 0);\n  },\n});\n```\n\n`renderCall` y `renderResult` reciben cada uno un objeto `context` con:\n- `args` - los argumentos de llamada de la herramienta actual\n- `state`: estado local de fila compartido en `renderCall` y `renderResult`\n- `lastComponent`: el componente devuelto anteriormente para esa ranura, si corresponde\n- `invalidate()`: solicita una nueva representación de esta fila de herramientas\n- `toolCallId`, `cwd`, `executionStarted`, `argsComplete`, `isPartial`, `expanded`, `showImages`, `isError`\n\nUtilice `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.\n\n#### renderLlamar\n\nRepresenta la llamada o encabezado de la herramienta:\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\nrenderCall(args, theme, context) {\n  const text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n  let content = theme.fg(\"toolTitle\", theme.bold(\"my_tool \"));\n  content += theme.fg(\"muted\", args.action);\n  if (args.text) {\n    content += \" \" + theme.fg(\"dim\", `\"${args.text}\"`);\n  }\n  text.setText(content);\n  return text;\n}\n```\n\n#### renderResultado\n\nRepresenta el resultado o salida de la herramienta:\n\n```typescript\nrenderResult(result, { expanded, isPartial }, theme, context) {\n  if (isPartial) {\n    return new Text(theme.fg(\"warning\", \"Processing...\"), 0, 0);\n  }\n\n  if (result.details?.error) {\n    return new Text(theme.fg(\"error\", `Error: ${result.details.error}`), 0, 0);\n  }\n\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (expanded && result.details?.items) {\n    for (const item of result.details.items) {\n      text += \"\\n  \" + theme.fg(\"dim\", item);\n    }\n  }\n  return new Text(text, 0, 0);\n}\n```\n\nSi un espacio no tiene contenido visible intencionalmente, devuelve un `Component` vacío, como un `Container` vacío.\n\n#### Sugerencias de combinación de teclas\n\nUtilice `keyHint()` para mostrar sugerencias de combinación de teclas que respeten la configuración de combinación de teclas activa:\n\n```typescript\nimport { keyHint } from \"@earendil-works/pi-coding-agent\";\n\nrenderResult(result, { expanded }, theme, context) {\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (!expanded) {\n    text += ` (${keyHint(\"app.tools.expand\", \"to expand\")})`;\n  }\n  return new Text(text, 0, 0);\n}\n```\n\nFunciones disponibles:\n- `keyHint(keybinding, description)`: formatea una identificación de combinación de teclas configurada como `\"app.tools.expand\"` o `\"tui.select.confirm\"`\n- `keyText(keybinding)`: devuelve el texto clave configurado sin formato para una identificación de combinación de teclas\n- `rawKeyHint(key, description)` - Formatear una cadena de clave sin formato\n\nUtilice identificadores de combinación de teclas con espacios de nombres:\n- Los identificadores de agente de codificación utilizan el espacio de nombres `app.*`, por ejemplo `app.tools.expand`, `app.editor.external`, `app.session.rename`\n- Los identificadores TUI compartidos utilizan el espacio de nombres `tui.*`, por ejemplo `tui.select.confirm`, `tui.select.cancel`, `tui.input.tab`\n\nPara obtener una lista exhaustiva de identificadores de combinaciones de teclas y valores predeterminados, consulte [keybindings.md](keybindings.md). `keybindings.json` usa esos mismos identificadores de espacio de nombres.\n\nLos 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()`.\n\n#### Mejores prácticas\n\n- Utilice `Text` con relleno `(0, 0)`. El cuadro predeterminado maneja el relleno.\n- Utilice `\\n` para contenido de varias líneas.\n- Maneje `isPartial` para ver el progreso de la transmisión.\n- Soporte `expanded` para obtener detalles a pedido.\n- Mantenga compacta la vista predeterminada.\n- Lea `context.args` en `renderResult` en lugar de copiar argumentos en `context.state`.\n- Utilice `context.state` solo para datos que deben compartirse entre espacios de llamadas y resultados.\n- Reutilice `context.lastComponent` cuando la misma instancia de componente se pueda actualizar en su lugar.\n- 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.\n\n#### Retroceder\n\nSi un renderizador de ranura no está definido o arroja:\n- `renderCall`: Muestra el nombre de la herramienta\n- `renderResult`: muestra texto sin formato de `content`\n\n### Carga dinámica de herramientas\n\nExtensions 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.\n\nEsto 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.\n\nEl ciclo de vida es:\n\n1. Registre cada herramienta con `pi.registerTool()` para que aparezca en `pi.getAllTools()`.\n2. Mantenga activas las herramientas de carga, como `search_tools`, y deje inactivas las herramientas de búsqueda.\n3. 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.\n4. Pi registra qué herramientas se agregaron en el resultado de la herramienta del cargador.\n5. 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.\n\nNo 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.\n\n#### Models con carga diferida nativa\n\n- **Antrópico**\n  - **Models:** Sonnet, Opus, Fable versión 4.5 o posterior (sin Haiku)\n  - **Representación nativa:** Las definiciones diferidas utilizan `defer_loading`; el punto de carga utiliza contenido `tool_reference`.\n- **AI abierta**\n  - **Models:** `gpt-5.4` y familia más nueva\n  - **Representación nativa:** Pi agrega los elementos completos del cliente `tool_search_call` y `tool_search_output` en el punto de carga.\n\nPara 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.\n\n#### Comportamiento alternativo\n\nPara 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.\n\nPi 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.\n\nPara 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.\n\n#### Ejemplo de herramienta de búsqueda\n\nLa 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.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nconst SEARCHABLE_TOOL_NAMES = new Set([\"lookup_weather\", \"search_issues\"]);\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerTool({\n    name: \"lookup_weather\",\n    label: \"Lookup Weather\",\n    description: \"Look up the current weather for a city\",\n    parameters: Type.Object({ city: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `Weather for ${params.city}: sunny` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_issues\",\n    label: \"Search Issues\",\n    description: \"Search project issues by keyword\",\n    parameters: Type.Object({ query: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `No open issues matching ${params.query}` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_tools\",\n    label: \"Search Tools\",\n    description: \"Search for and enable tools relevant to a task\",\n    promptSnippet: \"Search for additional tools when the active tools cannot perform the task\",\n    promptGuidelines: [\n      \"Use search_tools when a task requires a capability that is not currently available.\",\n    ],\n    parameters: Type.Object({\n      query: Type.String({ description: \"Capability or task to search for\" }),\n      limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),\n    }),\n    async execute(_toolCallId, params) {\n      const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);\n      const matches = pi.getAllTools()\n        .filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))\n        .map((tool) => ({\n          tool,\n          score: terms.reduce(\n            (score, term) =>\n              score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),\n            0,\n          ),\n        }))\n        .filter((match) => match.score > 0)\n        .sort((a, b) => b.score - a.score)\n        .slice(0, params.limit ?? 3)\n        .map((match) => match.tool.name);\n\n      if (matches.length === 0) {\n        return {\n          content: [{ type: \"text\", text: `No tools found for: ${params.query}` }],\n          details: { matches: [] },\n        };\n      }\n\n      const active = pi.getActiveTools();\n      const added = matches.filter((name) => !active.includes(name));\n      pi.setActiveTools([...new Set([...active, ...added])]);\n\n      return {\n        content: [{\n          type: \"text\",\n          text: added.length > 0\n            ? `Loaded tools: ${added.join(\", \")}`\n            : `Matching tools already active: ${matches.join(\", \")}`,\n        }],\n        details: { matches, added },\n      };\n    },\n  });\n\n  pi.on(\"session_start\", () => {\n    // Keep searchable tools registered but initially inactive. Preserve built-ins\n    // and tools owned by other extensions, and keep the loader itself active.\n    const initialTools = pi.getActiveTools().filter(\n      (name) => !SEARCHABLE_TOOL_NAMES.has(name),\n    );\n    pi.setActiveTools([...new Set([...initialTools, \"search_tools\"])]);\n  });\n}\n```\n\nCuando `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.\n\n## IU personalizada\n\nExtensions puede interactuar con los usuarios a través de métodos `ctx.ui` y personalizar la forma en que se muestran los mensajes/herramientas.\n\n**Para componentes personalizados, consulte [tui.md](tui.md)** que tiene patrones de copiar y pegar para:\n- Cuadros de diálogo de selección (SelectList)\n- Operaciones asíncronas con cancelación (BorderedLoader)\n- Alternancias de configuración (Lista de configuración)\n- Indicadores de estado (setStatus)\n- Mensaje de trabajo, visibilidad e indicador durante la transmisión (`setWorkingMessage`, `setWorkingVisible`, `setWorkingIndicator`)\n- Widgets encima/debajo del editor (setWidget)\n- Proveedores de autocompletar superpuestos a la finalización de ruta/barra incorporada (addAutocompleteProvider)\n- Pies de página personalizados (setFooter)\n\n### Diálogos\n\n```typescript\n// Select from options\nconst choice = await ctx.ui.select(\"Pick one:\", [\"A\", \"B\", \"C\"]);\n\n// Confirm dialog\nconst ok = await ctx.ui.confirm(\"Delete?\", \"This cannot be undone\");\n\n// Text input\nconst name = await ctx.ui.input(\"Name:\", \"placeholder\");\n\n// Multi-line editor\nconst text = await ctx.ui.editor(\"Edit:\", \"prefilled text\");\n\n// Notification (non-blocking)\nctx.ui.notify(\"Done!\", \"info\");  // \"info\" | \"warning\" | \"error\"\n```\n\n#### Diálogos cronometrados con cuenta regresiva\n\nLos cuadros de diálogo admiten una opción `timeout` que se cierra automáticamente con una visualización de cuenta regresiva en vivo:\n\n```typescript\n// Dialog shows \"Title (5s)\" → \"Title (4s)\" → ... → auto-dismisses at 0\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { timeout: 5000 }\n);\n\nif (confirmed) {\n  // User confirmed\n} else {\n  // User cancelled or timed out\n}\n```\n\n**Valores devueltos en el tiempo de espera:**\n- `select()` devuelve `undefined`\n- `confirm()` devuelve `false`\n- `input()` devuelve `undefined`\n\n#### Despido manual con AbortSignal\n\nPara tener más control (por ejemplo, para distinguir el tiempo de espera de la cancelación del usuario), use `AbortSignal`:\n\n```typescript\nconst controller = new AbortController();\nconst timeoutId = setTimeout(() => controller.abort(), 5000);\n\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { signal: controller.signal }\n);\n\nclearTimeout(timeoutId);\n\nif (confirmed) {\n  // User confirmed\n} else if (controller.signal.aborted) {\n  // Dialog timed out\n} else {\n  // User cancelled (pressed Escape or selected \"No\")\n}\n```\n\nConsulte [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) para ver ejemplos completos.\n\n### Widgets, estado y pie de página\n\n```typescript\n// Status in footer (persistent until cleared)\nctx.ui.setStatus(\"my-ext\", \"Processing...\");\nctx.ui.setStatus(\"my-ext\", undefined);  // Clear\n\n// Working loader (shown during streaming)\nctx.ui.setWorkingMessage(\"Thinking deeply...\");\nctx.ui.setWorkingMessage();  // Restore default\nctx.ui.setWorkingVisible(false);  // Hide the built-in working loader row entirely\nctx.ui.setWorkingVisible(true);   // Show the built-in working loader row\n\n// Working indicator (shown during streaming)\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });  // Static dot\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\nctx.ui.setWorkingIndicator({ frames: [] });  // Hide indicator\nctx.ui.setWorkingIndicator();  // Restore default spinner\n\n// Widget above editor (default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n// Widget below editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\nctx.ui.setWidget(\"my-widget\", (tui, theme) => new Text(theme.fg(\"accent\", \"Custom\"), 0, 0));\nctx.ui.setWidget(\"my-widget\", undefined);  // Clear\n\n// Custom footer (replaces built-in footer entirely)\nctx.ui.setFooter((tui, theme) => ({\n  render(width) { return [theme.fg(\"dim\", \"Custom footer\")]; },\n  invalidate() {},\n}));\nctx.ui.setFooter(undefined);  // Restore built-in footer\n\n// Terminal title\nctx.ui.setTitle(\"pi - my-project\");\n\n// Editor text\nctx.ui.setEditorText(\"Prefill text\");\nconst current = ctx.ui.getEditorText();\n\n// Paste into editor (triggers paste handling, including collapse for large content)\nctx.ui.pasteToEditor(\"pasted content\");\n\n// Stack custom autocomplete behavior on top of the built-in provider\nctx.ui.addAutocompleteProvider((current) => ({\n  triggerCharacters: [\"#\"],\n  async getSuggestions(lines, line, col, options) {\n    const beforeCursor = (lines[line] ?? \"\").slice(0, col);\n    const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n    if (!match) {\n      return current.getSuggestions(lines, line, col, options);\n    }\n\n    return {\n      prefix: `#${match[1] ?? \"\"}`,\n      items: [{ value: \"#2983\", label: \"#2983\", description: \"Extension API for autocomplete\" }],\n    };\n  },\n  applyCompletion(lines, line, col, item, prefix) {\n    return current.applyCompletion(lines, line, col, item, prefix);\n  },\n  shouldTriggerFileCompletion(lines, line, col) {\n    return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;\n  },\n}));\n\n// Tool output expansion\nconst wasExpanded = ctx.ui.getToolsExpanded();\nctx.ui.setToolsExpanded(true);\nctx.ui.setToolsExpanded(wasExpanded);\n\n// Custom editor (vim mode, emacs mode, etc.)\nctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));\nconst currentEditor = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))\n);\nctx.ui.setEditorComponent(undefined);  // Restore default editor\n\n// Theme management (see themes.md for creating themes)\nconst themes = ctx.ui.getAllThemes();  // [{ name: \"dark\", path: \"/...\" | undefined }, ...]\nconst lightTheme = ctx.ui.getTheme(\"light\");  // Load without switching\nconst result = ctx.ui.setTheme(\"light\");  // Switch by name\nif (!result.success) {\n  ctx.ui.notify(`Failed: ${result.error}`, \"error\");\n}\nctx.ui.setTheme(lightTheme!);  // Or switch by Theme object\nctx.ui.theme.fg(\"accent\", \"styled text\");  // Access current theme\n```\n\nLos 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(...)`.\n\n### Autocompletar Providers\n\nUtilice `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. Configure `triggerCharacters` para activadores naturales personalizados como.\n\nPatrón típico:\n\n- inspeccionar el texto antes del cursor\n- devolver sus propias sugerencias cuando la sintaxis específica de su extensión coincida\n- de lo contrario, delega a `current.getSuggestions(...)`\n- delegar `applyCompletion(...)` a menos que necesite un comportamiento de inserción personalizado\n\n```typescript\npi.on(\"session_start\", (_event, ctx) => {\n  ctx.ui.addAutocompleteProvider((current) => ({\n    triggerCharacters: [\"#\"],\n    async getSuggestions(lines, cursorLine, cursorCol, options) {\n      const line = lines[cursorLine] ?? \"\";\n      const beforeCursor = line.slice(0, cursorCol);\n      const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n      if (!match) {\n        return current.getSuggestions(lines, cursorLine, cursorCol, options);\n      }\n\n      return {\n        prefix: `#${match[1] ?? \"\"}`,\n        items: [\n          { value: \"#2983\", label: \"#2983\", description: \"Extension API for registering custom @ autocomplete providers\" },\n          { value: \"#2753\", label: \"#2753\", description: \"Reload stale resource settings\" },\n        ],\n      };\n    },\n\n    applyCompletion(lines, cursorLine, cursorCol, item, prefix) {\n      return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);\n    },\n\n    shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {\n      return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;\n    },\n  }));\n});\n```\n\nConsulte [github-issue-autocomplete.ts](../examples/extensions/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.\n\n### Componentes personalizados\n\nPara una interfaz de usuario compleja, utilice `ctx.ui.custom()`. Esto reemplaza temporalmente el editor con su componente hasta que se llame a `done()`:\n\n```typescript\nimport { Text, Component } from \"@earendil-works/pi-tui\";\n\nconst result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {\n  const text = new Text(\"Press Enter to confirm, Escape to cancel\", 1, 1);\n\n  text.onKey = (key) => {\n    if (key === \"return\") done(true);\n    if (key === \"escape\") done(false);\n    return true;\n  };\n\n  return text;\n});\n\nif (result) {\n  // User pressed Enter\n}\n```\n\nLa devolución de llamada recibe:\n- `tui` - TUI instancia (para dimensiones de pantalla, gestión de enfoque)\n- `theme` - Tema actual para estilizar\n- `keybindings` - Administrador de combinaciones de teclas de la aplicación (para comprobar los accesos directos)\n- `done(value)` - Llamada para cerrar el componente y devolver el valor\n\nConsulte [tui.md](tui.md) para ver el componente completo API.\n\n#### Modo de superposición (experimental)\n\nPase `{ overlay: true }` para representar el componente como un modal flotante encima del contenido existente, sin borrar la pantalla:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  { overlay: true }\n);\n```\n\nPara posicionamiento avanzado (anclajes, márgenes, porcentajes, visibilidad receptiva), pase `overlayOptions`. Utilice `onHandle` para controlar el enfoque o la visibilidad mediante programación:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: { anchor: \"top-right\", width: \"50%\", margin: 2 },\n    onHandle: (handle) => {\n      handle.focus(); // focus this overlay and bring it to the visual front\n      // handle.unfocus({ target: editorComponent }); // release input to a specific component\n      // handle.setHidden(true/false); // toggle visibility\n      // handle.hide(); // permanently remove\n    }\n  }\n);\n```\n\nUna 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.\n\nConsulte [tui.md](tui.md) para ver los `OverlayOptions` y `OverlayHandle` completos API y [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) para ver ejemplos.\n\n### Editor personalizado\n\nReemplace el editor de entrada principal con una implementación personalizada (modo vim, modo emacs, etc.):\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey } from \"@earendil-works/pi-tui\";\n\nclass VimEditor extends CustomEditor {\n  private mode: \"normal\" | \"insert\" = \"insert\";\n\n  handleInput(data: string): void {\n    if (matchesKey(data, \"escape\") && this.mode === \"insert\") {\n      this.mode = \"normal\";\n      return;\n    }\n    if (this.mode === \"normal\" && data === \"i\") {\n      this.mode = \"insert\";\n      return;\n    }\n    super.handleInput(data);  // App keybindings + text editing\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**Puntos clave:**\n- Extienda `CustomEditor` (no la base `Editor`) para obtener combinaciones de teclas de aplicaciones (escapar para cancelar, Ctrl+d, cambio de modelo)\n- Llame al `super.handleInput(data)` para llaves que no maneja\n- La fábrica recibe `tui`, `theme` y `keybindings` de la aplicación\n- Utilice `ctx.ui.getEditorComponent()` antes de `setEditorComponent()` para ajustar el editor personalizado previamente configurado\n- Pase `undefined` para restaurar el valor predeterminado: `ctx.ui.setEditorComponent(undefined)`\n\nPara componer con otra extensión que ya reemplazó al editor, captura la fábrica anterior antes de configurar la tuya:\n\n```typescript\nconst previous = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })\n);\n```\n\nConsulte [tui.md](tui.md) Patrón 7 para ver un ejemplo completo con indicador de modo.\n\n### Representación de mensajes y entradas\n\nRegistre un renderizador personalizado para mensajes con su `customType`. Utilice renderizadores de mensajes para contenido que debería participar en el contexto LLM:\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerMessageRenderer(\"my-extension\", (message, options, theme) => {\n  const { expanded, outputPad } = options;\n  let text = theme.fg(\"accent\", `[${message.customType}] `);\n  text += message.content;\n\n  if (expanded && message.details) {\n    text += \"\\n\" + theme.fg(\"dim\", JSON.stringify(message.details, null, 2));\n  }\n\n  return new Text(text, outputPad, 0);\n});\n```\n\nLos mensajes se envían a través de `pi.sendMessage()`:\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",  // Matches registerMessageRenderer\n  content: \"Status update\",\n  display: true,               // Show in TUI\n  details: { ... },            // Available in renderer\n});\n```\n\nPara contenido exclusivo de TUI que no debe enviarse al LLM, presente entradas personalizadas en su lugar:\n\n```typescript\npi.registerEntryRenderer(\"my-card\", (entry, options, theme) => {\n  return new Text(theme.fg(\"accent\", JSON.stringify(entry.data)));\n});\n\npi.appendEntry(\"my-card\", { status: \"done\" });\n```\n\n### Colores del tema\n\nTodas las funciones de renderizado reciben un objeto `theme`. Consulte [themes.md](themes.md) para crear temas personalizados y la paleta de colores completa.\n\n```typescript\n// Foreground colors\ntheme.fg(\"toolTitle\", text)   // Tool names\ntheme.fg(\"accent\", text)      // Highlights\ntheme.fg(\"success\", text)     // Success (green)\ntheme.fg(\"error\", text)       // Errors (red)\ntheme.fg(\"warning\", text)     // Warnings (yellow)\ntheme.fg(\"muted\", text)       // Secondary text\ntheme.fg(\"dim\", text)         // Tertiary text\n\n// Text styles\ntheme.bold(text)\ntheme.italic(text)\ntheme.strikethrough(text)\n```\n\nPara resaltar la sintaxis en los renderizadores de herramientas personalizadas:\n\n```typescript\nimport { highlightCode, getLanguageFromPath } from \"@earendil-works/pi-coding-agent\";\n\n// Highlight code with explicit language\nconst highlighted = highlightCode(\"const x = 1;\", \"typescript\", theme);\n\n// Auto-detect language from file path\nconst lang = getLanguageFromPath(\"/path/to/file.rs\");  // \"rust\"\nconst highlighted = highlightCode(code, lang, theme);\n```\n\n## Manejo de errores\n\n- Se registran errores de extensión, el agente continúa\n- `tool_call` los errores bloquean la herramienta (a prueba de fallos)\n- 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\n\n## Comportamiento del modo\n\n| Modo | `ctx.mode` | `ctx.hasUI` | Notas |\n|------|------------|-------------|-------|\n| Interactivo | `\"tui\"` | `true` | Completo TUI con renderizado de terminal |\n| RPC (`--mode rpc`) | `\"rpc\"` | `true` | Diálogos y notificaciones mediante protocolo JSON; `custom()` devuelve `undefined`. Ver [rpc.md](rpc.md) |\n| JSON (`--mode json`) | `\"json\"` | `false` | Transmisión de eventos a stdout; Los métodos de UI no son operativos |\n| Imprimir (`-p`) | `\"print\"` | `false` | Extensions se ejecuta pero no aparece |\n\nUtilice `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.\n\n## Referencia de ejemplos\n\nTodos los ejemplos en [examples/extensions/](../examples/extensions/).\n\n| Ejemplo | Descripción | Tecla APIs |\n|---------|-------------|----------|\n| **Herramientas** |  |  |\n| `hello.ts` | Registro mínimo de herramientas | `registerTool` |\n| `question.ts` | Herramienta con interacción del usuario. | `registerTool`, `ui.select` |\n| `questionnaire.ts` | Herramienta asistente de varios pasos | `registerTool`, `ui.custom` |\n| `todo.ts` | Herramienta con estado y persistencia | `registerTool`, `appendEntry`, `renderResult`, eventos de sesión |\n| `dynamic-tools.ts` | Registrar herramientas después del inicio y durante los comandos | `registerTool`, `session_start`, `registerCommand` |\n| `structured-output.ts` | Herramienta final de salida estructurada con `terminate: true` | `registerTool`, terminando los resultados de la herramienta |\n| `truncated-tool.ts` | Ejemplo de truncamiento de salida | `registerTool`, `truncateHead` |\n| `tool-override.ts` | Anular la herramienta de lectura incorporada | `registerTool` (mismo nombre que el integrado) |\n| **Comandos** |  |  |\n| `pirate.ts` | Modificar el mensaje del sistema por turno | `registerCommand`, `before_agent_start` |\n| `summarize.ts` | Comando de resumen de conversación | `registerCommand`, `ui.custom` |\n| `handoff.ts` | Transferencia de modelo entre proveedores | `registerCommand`, `ui.editor`, `ui.custom` |\n| `qna.ts` | Preguntas y respuestas con interfaz de usuario personalizada | `registerCommand`, `ui.custom`, `setEditorText` |\n| `send-user-message.ts` | Inyectar mensajes de usuario | `registerCommand`, `sendUserMessage` |\n| `reload-runtime.ts` | Recargar comando y transferencia de herramientas LLM | `registerCommand`, `ctx.reload()`, `sendUserMessage` |\n| `shutdown-command.ts` | Comando de apagado elegante | `registerCommand`, `shutdown()` |\n| **Eventos y puertas** |  |  |\n| `permission-gate.ts` | Bloquear comandos peligrosos | `on(\"tool_call\")`, `ui.confirm` |\n| `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 |\n| `protected-paths.ts` | Bloquear escrituras en rutas específicas | `on(\"tool_call\")` |\n| `confirm-destructive.ts` | Confirmar cambios de sesión | `on(\"session_before_switch\")`, `on(\"session_before_fork\")` |\n| `dirty-repo-guard.ts` | Advertir sobre repositorio git sucio | `on(\"session_before_*\")`, `exec` |\n| `input-transform.ts` | Transformar la entrada del usuario | `on(\"input\")` |\n| `input-transform-streaming.ts` | Transformación de entrada compatible con streaming | `on(\"input\")`, `streamingBehavior` |\n| `model-status.ts` | React para modelar cambios | `on(\"model_select\")`, `setStatus` |\n| `provider-payload.ts` | Inspeccionar cargas útiles y encabezados de respuesta del proveedor | `on(\"before_provider_request\")`, `on(\"after_provider_response\")` |\n| `system-prompt-header.ts` | Mostrar información de aviso del sistema | `on(\"agent_start\")`, `getSystemPrompt` |\n| `claude-rules.ts` | Cargar reglas desde archivos | `on(\"session_start\")`, `on(\"before_agent_start\")` |\n| `prompt-customizer.ts` | Agregue guía de herramientas contextual usando `systemPromptOptions` | `on(\"before_agent_start\")`, `BuildSystemPromptOptions` |\n| `file-trigger.ts` | El observador de archivos activa mensajes | `sendMessage` |\n| **Compactación y Sesiones** |  |  |\n| `custom-compaction.ts` | Resumen de compactación personalizado | `on(\"session_before_compact\")` |\n| `trigger-compact.ts` | Activar la compactación manualmente | `compact()` |\n| `git-checkpoint.ts` | Git esconderse en turnos | `on(\"turn_start\")`, `on(\"session_before_fork\")`, `exec` |\n| `git-merge-and-resolve.ts` | Recuperar, fusionar y resolver conflictos | `on(\"agent_end\")`, `exec`, `sendUserMessage` |\n| `auto-commit-on-exit.ts` | Comprometerse al cierre | `on(\"session_shutdown\")`, `exec` |\n| **Componentes de la interfaz de usuario** |  |  |\n| `status-line.ts` | Indicador de estado del pie de página | `setStatus`, eventos de sesión |\n| `working-indicator.ts` | Personaliza el indicador de funcionamiento de streaming | `setWorkingIndicator`, `registerCommand` |\n| `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` |\n| `custom-footer.ts` | Reemplazar el pie de página por completo | `registerCommand`, `setFooter` |\n| `custom-header.ts` | Reemplazar encabezado de inicio | `on(\"session_start\")`, `setHeader` |\n| `modal-editor.ts` | Editor modal estilo Vim | `setEditorComponent`, `CustomEditor` |\n| `rainbow-editor.ts` | Estilo de editor personalizado | `setEditorComponent` |\n| `widget-placement.ts` | Widget arriba/abajo del editor | `setWidget` |\n| `overlay-test.ts` | Componentes de superposición | `ui.custom` con opciones de superposición |\n| `overlay-qa-tests.ts` | Pruebas de superposición completas | `ui.custom`, todas las opciones de superposición |\n| `notify.ts` | Notificaciones simples | `ui.notify` |\n| `timed-confirm.ts` | Diálogos con tiempo de espera | `ui.confirm` con tiempo de espera/señal |\n| `mac-system-theme.ts` | Tema de cambio automático | `setTheme`, `exec` |\n| **Complejo Extensions** |  |  |\n| `plan-mode/` | Implementación del modo de plan completo | Todos los tipos de eventos, `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` |\n| `preset.ts` | Ajustes preestablecidos guardables (modelo, herramientas, pensamiento) | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` |\n| `tools.ts` | Activar o desactivar herramientas en la interfaz de usuario | `registerCommand`, `setActiveTools`, `SettingsList`, eventos de sesión |\n| **Remoto y zona de pruebas** |  |  |\n| `ssh.ts` | SSH ejecución remota | `registerFlag`, `on(\"user_bash\")`, `on(\"before_agent_start\")`, operaciones de herramienta |\n| `interactive-shell.ts` | Sesión de shell persistente | `on(\"user_bash\")` |\n| `sandbox/` | Ejecución de herramientas en espacio aislado | Operaciones de herramientas |\n| `gondolin/` | Enrute herramientas integradas y comandos `!` a una micro-VM Gondolin | Operaciones de herramientas, anulaciones de herramientas integradas, `on(\"user_bash\")` |\n| `subagent/` | Generar subagentes | `registerTool`, `exec` |\n| **Juegos** |  |  |\n| `snake.ts` | juego de serpiente | `registerCommand`, `ui.custom`, manejo del teclado |\n| `space-invaders.ts` | Juego Invasores Espaciales | `registerCommand`, `ui.custom` |\n| `doom-overlay/` | Perdición en superposición | `ui.custom` con superposición |\n| **Providers** |  |  |\n| `custom-provider-anthropic/` | Proxy antrópico personalizado | `registerProvider` |\n| `custom-provider-gitlab-duo/` | GitIntegración de Lab Duo | `registerProvider` con OAuth |\n| **Mensajes y comunicación** |  |  |\n| `message-renderer.ts` | Representación de mensajes personalizados | `registerMessageRenderer`, `sendMessage` |\n| `entry-renderer.ts` | TUI representación de entrada personalizada únicamente | `registerEntryRenderer`, `appendEntry` |\n| `event-bus.ts` | Eventos entre extensiones | `pi.events` |\n| **Metadatos de la sesión** |  |  |\n| `session-name.ts` | Nombrar sesiones para el selector | `setSessionName`, `getSessionName` |\n| `bookmark.ts` | Marcar entradas para /tree | `setLabel` |\n| **Varios** |  |  |\n| `inline-bash.ts` | bash en línea en llamadas a herramientas | `on(\"tool_call\")` |\n| `bash-spawn-hook.ts` | Ajuste el comando bash, cwd y env antes de la ejecución | `createBashTool`, `spawnHook` |\n| `with-deps/` | Extensión con dependencias npm | Estructura del paquete con `package.json` |","sourceFile":"extensions.md"},"index":{"title":"Pi Documentación","markdown":"Pi es un arnés de codificación de terminal mínimo. Está diseñado para permanecer pequeño en su núcleo mientras se amplía a través de TypeScript extensiones, habilidades, prompt templates, temas y paquetes pi.\n\n## Inicio rápido\n\nInstale Pi con npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` deshabilita los scripts del ciclo de vida de las dependencias durante la instalación. Pi no requiere scripts de instalación para instalaciones normales npm.\n\nEn Linux o macOS, también puedes utilizar el instalador:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nPara desinstalar pi, use npm para curl y npm instala:\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\nPara instalaciones pnpm, Yarn o Bun, utilice el comando de eliminación global correspondiente: `pnpm remove -g @earendil-works/pi-coding-agent`, `yarn global remove @earendil-works/pi-coding-agent` o `bun uninstall -g @earendil-works/pi-coding-agent`.\n\nLuego ejecútelo en un directorio de proyecto:\n\n```bash\npi\n```\n\nAutentíquese con `/login` para subscription providers, o establezca un API key como `ANTHROPIC_API_KEY` antes de iniciar pi.\n\nPara conocer el flujo completo de la primera ejecución, consulte [Quickstart](quickstart.md).\n\n## Empieza aquí\n\n- [Quickstart](quickstart.md): instala, autentica y ejecuta una primera sesión.\n- [Using Pi](usage.md) - modo interactivo, referencia slash commands, context files y CLI.\n- [Providers](providers.md): suscripción y configuración de clave API para proveedores integrados.\n- [llama.cpp](llama-cpp.md): ejecuta un enrutador local y administra modelos con `/llama`.\n- [Security](security.md): confianza en el proyecto, sandbox límites e informes de vulnerabilidad.\n- [Containerization](containerization.md) - sandbox pi con Gondolin, Docker o OpenShell.\n- [Settings](settings.md) - configuración global y del proyecto.\n- [Keybindings](keybindings.md): atajos predeterminados y combinaciones de teclas personalizadas.\n- [Sessions](sessions.md): gestión de sesiones, ramificación y navegación en árbol.\n- [Compaction](compaction.md) - context compaction y branch summarization.\n\n## Personalización\n\n- [Extensions](extensions.md) - TypeScript módulos para herramientas, comandos, eventos y UI personalizada.\n- [Skills](skills.md): Agente Skills para capacidades reutilizables bajo demanda.\n- [Prompt templates](prompt-templates.md): indicaciones reutilizables que se expanden desde slash commands.\n- [Themes](themes.md): integrado y personalizado terminal themes.\n- [Pi packages](packages.md): agrupa y comparte extensiones, habilidades, indicaciones y temas.\n- [Custom models](models.md): agrega entradas de modelo para el proveedor compatible APIs.\n- [Custom providers](custom-provider.md): implemente flujos API y OAuth personalizados.\n\n## Uso programático\n\n- [SDK](sdk.md): incrusta pi en las aplicaciones Node.js.\n- [RPC mode](rpc.md) - integrar sobre stdin/stdout JSONL.\n- [JSON event stream mode](json.md) - modo de impresión con eventos estructurados.\n- [TUI components](tui.md): crea una interfaz de usuario de terminal personalizada para extensiones.\n\n## Referencia\n\n- [Environment variables](environment-variables.md) - Pi configuración del proceso y metadatos de sesión disponibles para las herramientas bash.\n- [Session format](session-format.md) - JSONL formato de archivo de sesión, tipos de entrada y SessionManager API.\n\n## Configuración de la plataforma\n\n- [Windows](windows.md)\n- [Termux on Android](termux.md)\n- [tmux](tmux.md)\n- [Terminal setup](terminal-setup.md)\n- [Shell aliases](shell-aliases.md)\n\n## Desarrollo\n\n- [Development](development.md): configuración local, estructura del proyecto y depuración.","sourceFile":"index.md"},"json":{"title":"JSON Modo de transmisión de eventos","markdown":"```bash\npi --mode json \"Your prompt\"\n```\n\nGenera todos los eventos de la sesión como JSON líneas a stdout. Útil para integrar pi en otras herramientas o interfaces de usuario personalizadas.\n\n## Tipos de eventos\n\nLos eventos de cable utilizan `JsonAgentSessionEvent`. coincide\n[`AgentSessionEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/agent-session.ts)\nexcepto que las actualizaciones de mensajes en streaming omiten instantáneas acumulativas:\n\n```typescript\ntype WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, \"partial\"> : T;\n\ntype JsonAgentSessionEvent =\n  | Exclude<AgentSessionEvent, { type: \"message_update\" }>\n  | {\n      type: \"message_update\";\n      assistantMessageEvent: WithoutPartial<AssistantMessageEvent>;\n    };\n```\n\n`queue_update` emite todas las colas de dirección y seguimiento pendientes cada vez que cambian. `compaction_start` y `compaction_end` cubren la compactación tanto manual como automática.\n\nOtros eventos base provienen de\n[`AgentEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts):\n\n```typescript\ntype AgentEvent =\n  // Agent lifecycle\n  | { type: \"agent_start\" }\n  | { type: \"agent_end\"; messages: AgentMessage[] }\n  // Turn lifecycle\n  | { type: \"turn_start\" }\n  | { type: \"turn_end\"; message: AgentMessage; toolResults: ToolResultMessage[] }\n  // Message lifecycle\n  | { type: \"message_start\"; message: AgentMessage }\n  | { type: \"message_update\"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }\n  | { type: \"message_end\"; message: AgentMessage }\n  // Tool execution\n  | { type: \"tool_execution_start\"; toolCallId: string; toolName: string; args: any }\n  | { type: \"tool_execution_update\"; toolCallId: string; toolName: string; args: any; partialResult: any }\n  | { type: \"tool_execution_end\"; toolCallId: string; toolName: string; result: any; isError: boolean };\n```\n\n## Tipos de mensajes\n\nMensajes base de [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts#L134):\n- `UserMessage` (línea 134)\n- `AssistantMessage` (línea 140)\n- `ToolResultMessage` (línea 152)\n\nMensajes extendidos de [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts#L29):\n- `BashExecutionMessage` (línea 29)\n- `CustomMessage` (línea 46)\n- `BranchSummaryMessage` (línea 55)\n- `CompactionSummaryMessage` (línea 62)\n\n## Formato de salida\n\nCada línea es un objeto JSON. La primera línea es el encabezado de la sesión:\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"...\",\"cwd\":\"/path\"}\n```\n\nSeguido de los acontecimientos a medida que ocurren:\n\n```json\n{\"type\":\"agent_start\"}\n{\"type\":\"turn_start\"}\n{\"type\":\"message_start\",\"message\":{\"role\":\"assistant\",\"content\":[],...}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_end\",\"message\":{...}}\n{\"type\":\"turn_end\",\"message\":{...},\"toolResults\":[]}\n{\"type\":\"agent_end\",\"messages\":[...]}\n```\n\n`message_update` los registros son solo delta. Omiten tanto el campo acumulativo `message` como\n`assistantMessageEvent.partial` para mantener lineal el tamaño de la transmisión. Utilice `contentIndex` y `delta`\npara reunir argumentos de texto en vivo, pensamiento o llamada de herramientas si es necesario. `message_end` contiene\nel mensaje final autorizado.\n\n## Ejemplo\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```","sourceFile":"json.md"},"keybindings":{"title":"Combinaciones de teclas","markdown":"Todos los atajos de teclado se pueden personalizar mediante `~/.pi/agent/keybindings.json`. Cada acción puede estar vinculada a una o más claves.\n\nEl archivo de configuración usa los mismos identificadores de combinación de teclas con espacios de nombres que pi usa internamente y que los autores de extensiones usan en los administradores `keyHint()` e inyectados `keybindings`.\n\nLas configuraciones más antiguas que utilizan identificadores con espacios de nombres previos, como `cursorUp` o `expandTools`, se migran automáticamente a los identificadores con espacios de nombres al inicio.\n\nDespués de editar `keybindings.json`, ejecute `/reload` en pi para aplicar los cambios sin reiniciar la sesión.\n\n## Formato de clave\n\n`modifier+key` donde los modificadores son `ctrl`, `shift`, `alt`, `super` (combinables) y las claves son:\n\n- **Letras:** `a-z`\n- **Dígitos:** `0-9`\n- **Teclas especiales:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **Teclas de función:** `f1`-`f12`\n- **Símbolos:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\nCombinaciones de modificadores: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, `ctrl+1`, etc.\n\nLos enlaces `super` requieren un terminal que informe el modificador por separado, generalmente a través del protocolo de teclado Kitty. Es posible que no funcionen en terminales sin ese soporte.\n\n## Todas las acciones\n\n### TUI Movimiento del cursor del editor\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `tui.editor.cursorUp` | `up` | Mueva el cursor hacia arriba y explore el historial anterior en la parte superior |\n| `tui.editor.cursorDown` | `down` | Mueve el cursor hacia abajo y explora el historial más reciente en la parte inferior. |\n| `tui.editor.historyPrevious` | *(ninguno)* | Seleccione la entrada anterior del historial de mensajes |\n| `tui.editor.historyNext` | *(ninguno)* | Seleccione la siguiente entrada del historial de mensajes |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | Mover el cursor hacia la izquierda |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | Mover el cursor hacia la derecha |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Mover la palabra del cursor hacia la izquierda |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Mover la palabra del cursor hacia la derecha |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+home`, `ctrl+a` | Ir al inicio de la línea |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+end`, `ctrl+e` | Mover al final de la línea |\n| `tui.editor.jumpForward` | `ctrl+]` | Saltar al personaje |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | Saltar hacia atrás al personaje |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | Desplazarse hacia arriba por página |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | Desplazarse hacia abajo por página |\n\nLas acciones de historial dedicadas siempre cambian las entradas del historial, independientemente de la posición del cursor en un mensaje de varias líneas. Los enlaces de historial explícitos tienen prioridad sobre las acciones de la aplicación mientras el editor principal está enfocado, por lo que el enlace `tui.editor.historyPrevious` a `ctrl+p` anula el ciclo del modelo en ese contexto sin cambiar `Ctrl+P` en los selectores.\n\n### TUI Eliminación del editor\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `tui.editor.deleteCharBackward` | `backspace` | Eliminar carácter hacia atrás |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Eliminar carácter adelante |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Eliminar palabra al revés |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Eliminar palabra adelante |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | Eliminar al inicio de la línea |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | Eliminar al final de la línea |\n\n### TUI Entrada\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Insertar nueva línea |\n| `tui.input.submit` | `enter` | Enviar entrada |\n| `tui.input.tab` | `tab` | Pestaña/autocompletar |\n\n### TUI Anillo de muerte\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `tui.editor.yank` | `ctrl+y` | Pegar el texto eliminado más recientemente |\n| `tui.editor.yankPop` | `alt+y` | Recorrer el texto eliminado después de tirar |\n| `tui.editor.undo` | `ctrl+-` | Deshacer la última edición |\n\n### TUI Portapapeles y selección\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `tui.input.copy` | `ctrl+c` | Copiar selección |\n| `tui.select.up` | `up` | Mover selección hacia arriba |\n| `tui.select.down` | `down` | Mover selección hacia abajo |\n| `tui.select.pageUp` | `pageUp` | Página arriba en la lista |\n| `tui.select.pageDown` | `pageDown` | Avanzar página en la lista |\n| `tui.select.confirm` | `enter` | Confirmar selección |\n| `tui.select.cancel` | `escape`, `ctrl+c` | Cancelar selección |\n\n### TUI Ventana gráfica de pantalla completa\n\nEstas acciones se aplican cuando el modo interactivo usa `--tui-mode fullscreen` y apunta a la región de desplazamiento de la transcripción principal. El trackpad con dos dedos y la entrada con la rueda del mouse desplazan la región debajo del puntero, volviendo a la transcripción sobre el editor fijo/estado/pie de página. Al hacer clic en un hipervínculo OSC 8, se abre en el controlador predeterminado. Arrastrar con el botón principal del mouse selecciona el texto y lo copia en el portapapeles; Al mantener presionado el borde superior o inferior de la transcripción, se desplaza automáticamente hacia el contenido fuera de la pantalla.\n\nLos enlaces de transcripción de pantalla completa tienen prioridad sobre los enlaces del editor. Por lo tanto, las teclas de navegación predeterminadas no modificadas controlan la transcripción en modo de pantalla completa, mientras que sus variantes `ctrl` continúan controlando el editor. Fuera del modo de pantalla completa, ambas variantes controlan el editor.\n\n| Llave | Modo predeterminado | Modo de pantalla completa |\n|-----|--------------|-----------------|\n| `home`, `end` | Editor | Transcripción |\n| `ctrl+home`, `ctrl+end` | Editor | Editor |\n| `pageUp`, `pageDown` | Editor | Transcripción |\n| `ctrl+pageUp`, `ctrl+pageDown` | Editor | Editor |\n\nEste enrutamiento sigue siendo configurable a través de los enlaces de acción ordinarios. Por ejemplo, `\"tui.altScreen.pageUp\": \"ctrl+pageUp\"` hace que `pageUp` controle el editor y `ctrl+pageUp` controle la transcripción en modo de pantalla completa. Enlace `tui.altScreen.halfPageUp` y `tui.altScreen.halfPageDown` para pasos de transcripción más pequeños manteniendo los enlaces de página completa. La configuración `\"tui.altScreen.pageUp\": []` desactiva por completo ese acceso directo a la transcripción. Los enlaces de usuario reemplazan los valores predeterminados para esa acción.\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `tui.altScreen.pageUp` | `pageUp` | Desplazar la transcripción una página hacia arriba |\n| `tui.altScreen.pageDown` | `pageDown` | Desplácese hacia abajo en la transcripción una página |\n| `tui.altScreen.halfPageUp` | *(ninguno)* | Desplace la transcripción media página hacia arriba |\n| `tui.altScreen.halfPageDown` | *(ninguno)* | Desplaza la transcripción media página hacia abajo. |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up` | Saltar al mensaje marcado anterior |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down` | Saltar al siguiente mensaje marcado |\n| `tui.altScreen.top` | `home` | Desplácese hasta el comienzo de la transcripción. |\n| `tui.altScreen.bottom` | `end` | Desplácese hasta el final de la transcripción y siga el nuevo resultado. |\n\n### Solicitud\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | Cancelar / abortar |\n| `app.clear` | `ctrl+c` | Borrar editor (primero) / salir (segundo) |\n| `app.exit` | `ctrl+d` | Salir (cuando el editor esté vacío) |\n| `app.suspend` | `ctrl+z` (ninguno en Windows) | Suspender al fondo |\n| `app.editor.external` | `ctrl+g` | Abrir en un editor externo (`externalEditor`, `$VISUAL`, `$EDITOR`, Bloc de notas en Windows o `nano` en otro lugar) |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` en Windows) | Pegar imagen o texto desde el portapapeles |\n\n### Sesiones\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `app.session.new` | *(ninguno)* | Iniciar una nueva sesión (`/new`) |\n| `app.session.tree` | *(ninguno)* | Abrir navegador session tree (`/tree`) |\n| `app.session.fork` | *(ninguno)* | Bifurcar sesión actual (`/fork`) |\n| `app.session.resume` | *(ninguno)* | Selector de currículum de sesión abierta (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | Alternar visualización de ruta |\n| `app.session.toggleSort` | `ctrl+s` | Alternar modo de clasificación |\n| `app.session.toggleNamedFilter` | `ctrl+n` | Alternar filtro de solo nombre |\n| `app.session.rename` | `ctrl+r` | Cambiar nombre de sesión |\n| `app.session.delete` | `ctrl+d` | Eliminar sesión |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | Eliminar sesión cuando la consulta esté vacía |\n\n### Models y Pensando\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | Abrir selector de modelo |\n| `app.model.cycleForward` | `ctrl+p` | Pasar al siguiente modelo |\n| `app.model.cycleBackward` | `shift+ctrl+p` | Pasar al modelo anterior |\n| `app.thinking.cycle` | `shift+tab` | Nivel de pensamiento cíclico |\n| `app.thinking.toggle` | `ctrl+t` | Contraer o expandir bloques de pensamiento |\n\n### Cola de visualización y mensajes\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | Contraer o expandir la salida de la herramienta |\n| `app.message.copy` | `ctrl+x` | Copia el último mensaje del asistente o el mensaje seleccionado en `/tree` |\n| `app.message.followUp` | `alt+enter` | Mensaje de seguimiento de cola |\n| `app.message.dequeue` | `alt+up` | Restaurar mensajes en cola al editor |\n\n### Navegación por árbol\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Doblar el segmento de rama actual o saltar al inicio del segmento anterior |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Desplegar el segmento de rama actual o saltar al inicio del siguiente segmento o al final de la rama |\n| `app.tree.editLabel` | `shift+l` | Editar la etiqueta en el nodo del árbol seleccionado |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | Alternar marcas de tiempo de etiquetas en el árbol |\n| `app.tree.filter.default` | `ctrl+d` | Establecer el filtro de árbol en la vista predeterminada |\n| `app.tree.filter.noTools` | `ctrl+t` | Alternar filtro de árbol que oculta los resultados de la herramienta |\n| `app.tree.filter.userOnly` | `ctrl+u` | Alternar filtro de árbol que muestra solo mensajes de usuario |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | Alternar filtro de árbol que muestra solo entradas etiquetadas |\n| `app.tree.filter.all` | `ctrl+a` | Alternar filtro de árbol que muestra todas las entradas |\n| `app.tree.filter.cycleForward` | `ctrl+o` | Ciclo del filtro del árbol hacia adelante |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Ciclo del filtro del árbol hacia atrás |\n\n### Selector de alcance Models\n\nSe utiliza dentro del selector de modelos con alcance (se abre mediante `/scoped-models`).\n\n| ID de combinación de teclas | Por defecto | Descripción |\n|--------|---------|-------------|\n| `app.models.save` | `ctrl+s` | Guardar la selección del modelo actual en la configuración |\n| `app.models.enableAll` | `ctrl+a` | Habilitar todos los modelos (o todos los que coincidan con la búsqueda actual) |\n| `app.models.clearAll` | `ctrl+x` | Borrar todos los modelos (o todos los que coincidan con la búsqueda actual) |\n| `app.models.toggleProvider` | `ctrl+p` | Alternar todos los modelos para el proveedor actual |\n| `app.models.reorderUp` | `alt+up` | Mover el modelo seleccionado hacia arriba en el orden del ciclo. |\n| `app.models.reorderDown` | `alt+down` | Mover el modelo seleccionado hacia abajo en el orden del ciclo. |\n\n## Configuración personalizada\n\nCrear `~/.pi/agent/keybindings.json`:\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.deleteWordBackward\": [\"ctrl+w\", \"alt+backspace\"]\n}\n```\n\nCada acción puede tener una única clave o una serie de claves. La configuración del usuario anula los valores predeterminados.\n\nEn Windows nativo, `app.suspend` no tiene enlace predeterminado porque los terminales de Windows no admiten el control de trabajos de Unix. Si lo vincula manualmente, pi muestra un mensaje de estado en lugar de suspenderlo. En WSL, el comportamiento normal de Linux `ctrl+z`/`fg` todavía se aplica.\n\n### Ejemplo de Emacs\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.cursorLeft\": [\"left\", \"ctrl+b\"],\n  \"tui.editor.cursorRight\": [\"right\", \"ctrl+f\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+f\"],\n  \"tui.editor.deleteCharForward\": [\"delete\", \"ctrl+d\"],\n  \"tui.editor.deleteCharBackward\": [\"backspace\", \"ctrl+h\"],\n  \"tui.input.newLine\": [\"shift+enter\", \"ctrl+j\"]\n}\n```\n\n### Ejemplo de Vim\n\n```json\n{\n  \"tui.editor.cursorUp\": [\"up\", \"alt+k\"],\n  \"tui.editor.cursorDown\": [\"down\", \"alt+j\"],\n  \"tui.editor.cursorLeft\": [\"left\", \"alt+h\"],\n  \"tui.editor.cursorRight\": [\"right\", \"alt+l\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+w\"]\n}\n```","sourceFile":"keybindings.md"},"llama-cpp":{"title":"llama.cpp","markdown":"Pi admite el servidor enrutador [llama.cpp](https://github.com/ggml-org/llama.cpp). El enrutador descubre múltiples modelos GGUF y los carga o descarga según demanda.\n\nUtilice una compilación llama.cpp actual con soporte para enrutador. Siga el [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) o instale un [prebuilt release](https://github.com/ggml-org/llama.cpp/releases) para su plataforma.\n\n## Inicie el enrutador\n\nComience `llama-server` sin `--model` o `-m`. Al pasar un modelo se inicia el modo de modelo único en lugar del modo de enrutador.\n\n```bash\nllama-server \\\n  --models-dir ~/models \\\n  --no-models-autoload \\\n  --jinja \\\n  --host 127.0.0.1 \\\n  --port 8080 \\\n  -ngl 999 \\\n  -c 32768\n```\n\nOpciones importantes:\n\n- `--models-dir ~/models` descubre archivos GGUF locales.\n- `--no-models-autoload` sigue cargándose de forma explícita hasta `/llama`.\n- `--jinja` habilita plantillas de chat compatibles y llamadas de herramientas.\n- `-ngl 999` descarga tantas capas como sea posible a la GPU.\n- `-c 32768` establece la ventana de contexto para cada modelo cargado. Omítalo para utilizar el contexto nativo del modelo, que puede requerir mucha más memoria.\n\nUn modelo de un solo archivo puede ubicarse directamente en el directorio del modelo. Coloque los modelos multimodales y de múltiples fragmentos en subdirectorios separados:\n\n```text\n~/models/\n├── llama-3.2-1b-Q4_K_M.gguf\n├── gemma-3-4b-it-Q4_K_M/\n│   ├── gemma-3-4b-it-Q4_K_M.gguf\n│   └── mmproj-F16.gguf\n└── large-model-Q4_K_M/\n    ├── large-model-Q4_K_M-00001-of-00003.gguf\n    ├── large-model-Q4_K_M-00002-of-00003.gguf\n    └── large-model-Q4_K_M-00003-of-00003.gguf\n```\n\nReinicie el enrutador después de agregar archivos manualmente. Para tamaños de contexto por modelo y otras opciones, utilice [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets).\n\n## Configurar Pi\n\nInicie Pi y configure el proveedor:\n\n```text\n/login llama.cpp\n```\n\nIngrese la URL del enrutador y opcional API key. La URL predeterminada es `http://127.0.0.1:8080`.\n\nLas variables de entorno pueden configurar los mismos valores sin `/login`:\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\nSi el servidor usa un API key, comience `llama-server` con el valor `--api-key` correspondiente. Mantenga `--host 127.0.0.1` para acceso solo local.\n\n## Administrar modelos\n\nCorrer:\n\n```text\n/llama\n```\n\n- Seleccione un modelo descargado para cargarlo.\n- Seleccione un modelo cargado para descargarlo.\n- Seleccione **Descargar modelo…**, busque Hugging Face, luego elija un repositorio y una cuantificación. Los valores exactos de `owner/repository[:quant]` también funcionan.\n- Presione Escape durante una carga o descarga para confirmar la cancelación.\n\nLa búsqueda Hugging Face usa `HF_TOKEN` cuando está configurada, luego verifica `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token` y `~/.cache/huggingface/token`. La búsqueda también funciona sin autenticación, sujeta a límites de tarifas más bajos. Pi advierte antes de descargar repositorios privados y enlaces a su página de acceso. El servidor llama.cpp realiza la descarga, por lo que su proceso también debe tener `HF_TOKEN` cuando el repositorio seleccionado requiera acceso.\n\nSi se cargan otros modelos, Pi pregunta si desea descargarlos primero o mantenerlos cargados. Pi no descarga modelos silenciosamente y nunca elimina archivos de modelos. El enrutador se puede compartir con otros clientes, por lo que `/llama` siempre muestra el estado actual del enrutador.\n\nSolo los modelos cargados aparecen en `/model`. Después de cargar un modelo, ejecute `/model` para seleccionarlo para la sesión Pi actual.\n\nSi el enrutador se desconecta, `/llama` muestra **Reintentar** y **Cerrar**. Reintentar reconecta y actualiza el estado del modelo sin reproducir la operación interrumpida.\n\n## Solución de problemas\n\nVerifique que el enrutador sea accesible:\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **No hay modelos en `/llama`:** Verifique `--models-dir`, el diseño del directorio, y reinicie el enrutador.\n- **Falta el modelo de `/model`:** Cárgalo con `/llama` primero.\n- **La carga falla o usa demasiada memoria:** Baje `-c` o descargue otro modelo.\n- **El servidor no está en modo enrutador:** Inícielo sin `--model`, `-m` o `-hf`.","sourceFile":"llama-cpp.md"},"models":{"title":"Personalizado Models","markdown":"Agregue proveedores y modelos personalizados (Ollama, vLLM, LM Studio, proxies) a través de `~/.pi/agent/models.json`.\n\n## Tabla de contenido\n\n- [Minimal Example](#minimal-example)\n- [Full Example](#full-example)\n- [Supported APIs](#supported-apis)\n- [Provider Configuration](#provider-configuration)\n- [Model Configuration](#model-configuration)\n- [Overriding Built-in Providers](#overriding-built-in-providers)\n- [Per-model Overrides](#per-model-overrides)\n- [Anthropic Messages Compatibility](#anthropic-messages-compatibility)\n- [OpenAI Compatibility](#openai-compatibility)\n\n## Ejemplo mínimo\n\nPara modelos locales (Ollama, LM Studio, vLLM), solo se requiere `id` por modelo:\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        { \"id\": \"llama3.1:8b\" },\n        { \"id\": \"qwen2.5-coder:7b\" }\n      ]\n    }\n  }\n}\n```\n\nEl valor `apiKey` es un marcador de posición porque Ollama lo ignora. pi todavía trata los modelos como si requieren autenticación antes de que aparezcan en `/model`, por lo que los servidores locales sin clave deben mantener un valor ficticio, guardar una clave para ese proveedor con `/login` o pasar `--api-key` al seleccionar el modelo.\n\nAlgunos servidores compatibles con OpenAI no comprenden la función `developer` utilizada para modelos con capacidad de razonamiento. Para esos proveedores, configure `compat.supportsDeveloperRole` en `false` para que pi envíe el mensaje del sistema como un mensaje `system`. Si el servidor tampoco admite `reasoning_effort`, configure `compat.supportsReasoningEffort` en `false` también.\n\nPuede configurar `compat` a nivel de proveedor para aplicarlo a todos los modelos, o a nivel de modelo para anular un modelo específico. Esto comúnmente se aplica a Ollama, vLLM, SGLang y servidores similares compatibles con OpenAI.\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"compat\": {\n        \"supportsDeveloperRole\": false,\n        \"supportsReasoningEffort\": false\n      },\n      \"models\": [\n        {\n          \"id\": \"gpt-oss:20b\",\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n## Ejemplo completo\n\nAnule los valores predeterminados cuando necesite valores específicos:\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        {\n          \"id\": \"llama3.1:8b\",\n          \"name\": \"Llama 3.1 8B (Local)\",\n          \"reasoning\": false,\n          \"input\": [\"text\"],\n          \"contextWindow\": 128000,\n          \"maxTokens\": 32000,\n          \"cost\": { \"input\": 0, \"output\": 0, \"cacheRead\": 0, \"cacheWrite\": 0 }\n        }\n      ]\n    }\n  }\n}\n```\n\nEl archivo se recarga cada vez que abres `/model`. Editar durante la sesión; no es necesario reiniciar.\n\n## Ejemplo de estudio de IA de Google\n\nUtilice `google-generative-ai` con `baseUrl` para agregar modelos de Google AI Studio, incluidas entradas personalizadas de Gemma 4:\n\n```json\n{\n  \"providers\": {\n    \"my-google\": {\n      \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta\",\n      \"api\": \"google-generative-ai\",\n      \"apiKey\": \"$GEMINI_API_KEY\",\n      \"models\": [\n        {\n          \"id\": \"gemma-4-31b-it\",\n          \"name\": \"Gemma 4 31B\",\n          \"input\": [\"text\", \"image\"],\n          \"contextWindow\": 262144,\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\nEl `baseUrl` es necesario al agregar modelos personalizados al tipo `google-generative-ai` API.\n\n## Compatible APIs\n\n| API | Descripción |\n|-----|-------------|\n| `openai-completions` | Finalizaciones de OpenAI Chat (más compatibles) |\n| `openai-responses` | Respuestas de OpenAI API |\n| `anthropic-messages` | Mensajes antrópicos API |\n| `google-generative-ai` | IA generativa de Google |\n\nEstablezca `api` a nivel de proveedor (predeterminado para todos los modelos) o a nivel de modelo (anulación por modelo).\n\n## Configuración del proveedor\n\n| Campo | Descripción |\n|-------|-------------|\n| `baseUrl` | API URL del punto final |\n| `api` | API tipo (ver arriba) |\n| `apiKey` | Configuración API key opcional (consulte la resolución del valor a continuación). Omítalo cuando la autenticación la proporcione `/login`/`auth.json` o CLI `--api-key`. |\n| `oauth` | Tipo de proveedor dinámico OAuth. Actualmente admite `\"radius\"`; requiere la puerta de enlace `baseUrl`. |\n| `headers` | Encabezados personalizados (consulte la resolución de valores a continuación) |\n| `authHeader` | Configure `true` para agregar `Authorization: Bearer <apiKey>` automáticamente |\n| `models` | Matriz de configuraciones de modelos |\n| `modelOverrides` | Anulaciones por modelo para modelos integrados o registrados en extensión en este proveedor |\n\nPara los proveedores con `models`, las configuraciones de proveedores no integradas necesitan un valor `baseUrl` y un `api` a nivel de proveedor o de modelo. No es necesario `apiKey` para cargar el archivo: los modelos están disponibles cuando se configura la autenticación a través de `/login`/`auth.json`, CLI `--api-key` o el proveedor `apiKey`. Si no se configura ninguna autenticación, los modelos se cargan pero no están disponibles en `/model` y `--list-models`.\n\n### Resolución de valor\n\nLos campos `apiKey` y `headers` admiten la ejecución de comandos, la interpolación del entorno y los literales:\n\n- **Comando Shell:** `\"!command\"` al principio ejecuta el valor completo como un comando y usa stdout\n  ```json\n  \"apiKey\": \"!security find-generic-password -ws 'anthropic'\"\n  \"apiKey\": \"!op read 'op://vault/item/credential'\"\n  ```\n- **Interpolación del entorno:** `\"$ENV_VAR\"` o `\"${ENV_VAR}\"` usa el valor de la variable nombrada. La interpolación funciona dentro de literales más grandes.\n  ```json\n  \"apiKey\": \"$MY_API_KEY\"\n  \"apiKey\": \"${KEY_PREFIX}_${KEY_SUFFIX}\"\n  ```\n  `$FOO_BAR` es la variable `FOO_BAR`; use `${FOO}_BAR` cuando `BAR` sea texto literal. Las variables de entorno que faltan hacen que el valor no se resuelva.\n- **Escapa:** `\"$\"` emite un literal `\"$\"`; `\"$!\"` emite un literal `\"!\"` sin activar la ejecución del comando.\n  ```json\n  \"apiKey\": \"$$literal-dollar-prefix\"\n  \"apiKey\": \"$!literal-bang-prefix\"\n  ```\n- **Valor literal:** Usado directamente. Las cadenas simples en mayúsculas como `MY_API_KEY` son literales; utilice `$MY_API_KEY` para las variables de entorno.\n  ```json\n  \"apiKey\": \"sk-...\"\n  ```\n\nPara `models.json`, los comandos de shell se resuelven en el momento de la solicitud. pi intencionalmente no aplica TTL incorporado, reutilización obsoleta o lógica de recuperación para comandos arbitrarios. Diferentes comandos necesitan diferentes estrategias de almacenamiento en caché y fallas, y pi no puede inferir cuál es la correcta.\n\nSi su comando es lento, costoso, tiene una velocidad limitada o debe seguir usando un valor anterior en fallas transitorias, envuélvalo en su propio script o comando que implemente el comportamiento de almacenamiento en caché o TTL que desee.\n\n`/model` las comprobaciones de disponibilidad utilizan la presencia de autenticación configurada y no ejecutan comandos de shell.\n\n### Encabezados personalizados\n\n```json\n{\n  \"providers\": {\n    \"custom-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com/v1\",\n      \"apiKey\": \"$MY_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"headers\": {\n        \"x-portkey-api-key\": \"$PORTKEY_API_KEY\",\n        \"x-secret\": \"!op read 'op://vault/item/secret'\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n## Configuración del modelo\n\n| Campo | Requerido | Por defecto | Descripción |\n|-------|----------|---------|-------------|\n| `id` | Sí | — | Identificador de modelo (pasado al API) |\n| `name` | No | `id` | Etiqueta de modelo legible por humanos. Se utiliza para hacer coincidir (`--model` patrones) y se muestra como texto de detalle del modelo secundario. |\n| `api` | No | del proveedor `api` | Anular el API del proveedor para este modelo |\n| `reasoning` | No | `false` | Apoya el pensamiento extendido |\n| `thinkingLevelMap` | No | omitido | Asigna los niveles de pensamiento de pi a los valores del proveedor y marca los niveles no admitidos (ver más abajo) |\n| `input` | No | `[\"text\"]` | Tipos de entrada: `[\"text\"]` o `[\"text\", \"image\"]` |\n| `contextWindow` | No | `128000` | Tamaño de la ventana de contexto en tokens |\n| `maxTokens` | No | `16384` | Tokens de salida máximos |\n| `samplingParams` | No | omitido | Los parámetros de muestreo se fusionaron palabra por palabra en cada cuerpo de solicitud (ver más abajo) |\n| `cost` | No | todos ceros | Tarifas por millón de tokens con niveles de precios de entrada opcionales para toda la solicitud |\n| `compat` | No | proveedor `compat` | Anulaciones de compatibilidad de proveedores. Combinado con el nivel de proveedor `compat` cuando ambos están configurados. |\n\nUn nivel de costo proporciona un conjunto completo de tarifas alternativas y se aplica a la solicitud completa cuando el uso total de insumos (`input + cacheRead + cacheWrite`) excede `inputTokensAbove`. Cuando coinciden varios niveles, gana el umbral más alto.\n\n```json\n{\n  \"cost\": {\n    \"input\": 5,\n    \"output\": 30,\n    \"cacheRead\": 0.5,\n    \"cacheWrite\": 6.25,\n    \"tiers\": [\n      {\n        \"inputTokensAbove\": 272000,\n        \"input\": 10,\n        \"output\": 45,\n        \"cacheRead\": 1,\n        \"cacheWrite\": 12.5\n      }\n    ]\n  }\n}\n```\n\nComportamiento actual:\n- `/model`, `--list-models` y el pie de página interactivo muestran las entradas por modelo `id`.\n- El `name` configurado se utiliza para la coincidencia de modelos y el texto de detalles del modelo secundario. No reemplaza la identificación del modelo de pie de página/barra de estado.\n\n### Parámetros de muestreo\n\n`samplingParams` es un objeto de forma libre fusionado palabra por palabra en cada cuerpo de solicitud para el modelo, después de que los campos pi se configuran, por lo que sus claves ganan. Úselo para enviar parámetros de muestreo que pi no modela, incluidos los específicos del servidor como `min_p` de llama.cpp o `top_k` de vLLM:\n\n```json\n{\n  \"id\": \"deepseek-v4-flash\",\n  \"samplingParams\": {\n    \"temperature\": 1.0,\n    \"top_p\": 0.95,\n    \"top_k\": 0,\n    \"min_p\": 0.0\n  }\n}\n```\n\nSolo los API compatibles con OpenAI lo aplican (`openai-completions`, `openai-responses`, `azure-openai-responses`); otros API lo ignoran. Las claves anulan los campos de solicitud con nombre de pi (por ejemplo, una clave `temperature` aquí supera la temperatura del nivel de solicitud), así que prefiérala como la única fuente de verdad de muestreo para un modelo. En `modelOverrides`, `samplingParams` se fusiona por clave con el valor del modelo base.\n\n### Mapa de niveles de pensamiento\n\nUtilice `thinkingLevelMap` en un modelo para describir controles de pensamiento específicos del modelo. Las claves son los niveles de pensamiento pi: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Los mapas pueden contener agujeros; por ejemplo, un modelo puede exponer `high` y `max` sin exponer `xhigh`.\n\nLos valores son triples:\n\n| Valor | Significado |\n|-------|---------|\n| omitido | Los niveles estándar hasta `high` utilizan la asignación predeterminada del proveedor; Los niveles extendidos `xhigh` y `max` no son compatibles |\n| cadena | El nivel es compatible y este valor se envía al proveedor. |\n| `null` | El nivel no es compatible y está oculto/omitido/sujeto |\n\nEjemplo de un modelo que solo admite razonamiento apagado, alto y máximo:\n\n```json\n{\n  \"id\": \"deepseek-v4-pro\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"minimal\": null,\n    \"low\": null,\n    \"medium\": null,\n    \"high\": \"high\",\n    \"xhigh\": null,\n    \"max\": \"max\"\n  }\n}\n```\n\nEjemplo de un modelo en el que no se puede desactivar el pensamiento:\n\n```json\n{\n  \"id\": \"always-thinking-model\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"off\": null\n  }\n}\n```\n\nMigración: las configuraciones anteriores que usaban `compat.reasoningEffortMap` deberían mover esa asignación al nivel de modelo `thinkingLevelMap`. Utilice `null` para niveles que no deberían aparecer en la interfaz de usuario.\n\n## Anulación incorporada Providers\n\nEnrute un proveedor integrado a través de un proxy sin redefinir los modelos:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\"\n    }\n  }\n}\n```\n\nTodos los modelos Anthropic integrados siguen estando disponibles. La autenticación OAuth o API key existente continúa funcionando.\n\nPara fusionar modelos personalizados en un proveedor integrado, incluya la matriz `models`:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"models\": [...]\n    }\n  }\n}\n```\n\nFusionar semántica:\n- Se mantienen los modelos incorporados.\n- Los modelos personalizados se insertan en `id` dentro del proveedor.\n- Si un modelo personalizado `id` coincide con un modelo integrado `id`, el modelo personalizado reemplaza ese modelo integrado.\n- Si un modelo personalizado `id` es nuevo, se agrega junto con los modelos integrados.\n\n## Anulaciones por modelo\n\nUtilice `modelOverrides` para personalizar los modelos integrados y los modelos registrados en extensiones coincidentes sin reemplazar la lista completa de modelos del proveedor.\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"modelOverrides\": {\n        \"anthropic/claude-sonnet-4\": {\n          \"name\": \"Claude Sonnet 4 (Bedrock Route)\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"only\": [\"amazon-bedrock\"]\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n`modelOverrides` admite estos campos por modelo: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (parcial), `contextWindow`, `maxTokens`, `samplingParams` (combinado por clave), `headers`, `compat`.\n\nDirect OpenAI GPT-5.6 Sol, Terra y Luna tienen de forma predeterminada una ventana de contexto `272000` para que las solicitudes permanezcan dentro del nivel de precios de contexto corto de OpenAI. Para optar por la ventana contextual de 1,05 M de OpenAI, auméntela para cada modelo que utilice:\n\n```json\n{\n  \"providers\": {\n    \"openai\": {\n      \"modelOverrides\": {\n        \"gpt-5.6-sol\": {\n          \"contextWindow\": 1050000\n        }\n      }\n    }\n  }\n}\n```\n\nLa anulación conserva los metadatos de precios integrados. Las solicitudes con más de 272 000 tokens de entrada en total utilizan las tarifas de contexto largo de GPT-5.6 para toda la solicitud. Aplique la misma anulación a `gpt-5.6-terra` o `gpt-5.6-luna` cuando sea necesario.\n\nNotas de comportamiento:\n- `modelOverrides` se aplican a los modelos de proveedores integrados y a los modelos de proveedores registrados en extensiones coincidentes.\n- Se ignoran los ID de modelos desconocidos.\n- Puede combinar el nivel de proveedor `baseUrl`/`headers` con `modelOverrides`.\n- Anular `name` cambia la coincidencia del modelo y el texto de detalle secundario únicamente; el pie de página y las listas de modelos principales continúan mostrando el modelo `id`.\n- Si también se define `models` para un proveedor, los modelos personalizados se fusionan después de las anulaciones integradas. Un modelo personalizado con el mismo `id` reemplaza la entrada del modelo integrado anulado.\n\n## Compatibilidad de mensajes antrópicos\n\nPara proveedores o proxies que usan `api: \"anthropic-messages\"`, use `compat` para controlar la compatibilidad de solicitudes específicas de Anthropic.\n\nDe forma predeterminada, pi envía por herramienta `eager_input_streaming: true`. Si un proxy o un backend compatible con Anthropic rechaza ese campo, establezca `supportsEagerToolInputStreaming` en `false`. Pi omitirá `tools[].eager_input_streaming` y en su lugar enviará el encabezado beta heredado `fine-grained-tool-streaming-2025-05-14` para solicitudes habilitadas para herramientas.\n\nAlgunos modelos antrópicos requieren pensamiento adaptativo (`thinking.type: \"adaptive\"` más `output_config.effort`) en lugar del pensamiento heredado basado en el presupuesto. Los modelos integrados configuran esto automáticamente. Para proveedores personalizados o alias que se dirigen a esos modelos, establezca `forceAdaptiveThinking` en `true`.\n\nAlgunos proveedores compatibles con Anthropic emiten bloques de pensamiento con firmas vacías y aún esperan que se reproduzcan. Establezca `allowEmptySignature` en `true` solo para esos proveedores; El Antrópico real rechaza las firmas de pensamiento vacío.\n\nLos modelos antrópicos integrados habilitan `supportsStrictTools` en los metadatos de su modelo. Los modelos personalizados compatibles con Anthropic deben configurarlo en `true` cuando su punto final acepte definiciones estrictas de herramientas de esquema JSON.\n\n```json\n{\n  \"providers\": {\n    \"anthropic-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com\",\n      \"api\": \"anthropic-messages\",\n      \"apiKey\": \"$ANTHROPIC_PROXY_KEY\",\n      \"compat\": {\n        \"supportsEagerToolInputStreaming\": false,\n        \"supportsLongCacheRetention\": true,\n        \"forceAdaptiveThinking\": true,\n        \"allowEmptySignature\": true\n      },\n      \"models\": [\n        {\n          \"id\": \"claude-opus-4-7\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"]\n        }\n      ]\n    }\n  }\n}\n```\n\n| Campo | Descripción |\n|-------|-------------|\n| `supportsEagerToolInputStreaming` | Si el proveedor acepta por herramienta `eager_input_streaming`. Predeterminado: `true`. Configúrelo en `false` para omitir ese campo y usar el encabezado beta de transmisión detallada de la herramienta heredada en solicitudes habilitadas para la herramienta. |\n| `supportsLongCacheRetention` | Si el proveedor acepta la retención de caché larga antrópica (`cache_control.ttl: \"1h\"`) cuando la retención de caché es `long`. Predeterminado: `true`. |\n| `sendSessionAffinityHeaders` | Si se debe enviar `x-session-affinity` desde la identificación de la sesión cuando el almacenamiento en caché está habilitado. Valor predeterminado: detectado automáticamente para proveedores conocidos. |\n| `supportsCacheControlOnTools` | Si el proveedor acepta marcadores `cache_control` de estilo antrópico en las definiciones de herramientas. Predeterminado: `true`. |\n| `forceAdaptiveThinking` | Ya sea para enviar pensamiento adaptativo (`thinking.type: \"adaptive\"` más `output_config.effort`) para este modelo. Los modelos adaptativos integrados configuran esto automáticamente. Predeterminado: `false`. |\n| `allowEmptySignature` | Si se deben reproducir firmas de pensamiento vacías como `signature: \"\"` en lugar de convertir el pensamiento en texto. Predeterminado: `false`. |\n| `supportsStrictTools` | Si el proveedor acepta definiciones estrictas de herramientas de esquema JSON. Predeterminado: `false`; Los modelos antrópicos incorporados lo habilitan en los metadatos generados. |\n\n## Compatibilidad con OpenAI\n\nPara proveedores con compatibilidad parcial con OpenAI, utilice el campo `compat`.\n\n- El nivel de proveedor `compat` aplica los valores predeterminados a todos los modelos de ese proveedor.\n- El nivel de modelo `compat` anula los valores a nivel de proveedor para ese modelo.\n\n```json\n{\n  \"providers\": {\n    \"local-llm\": {\n      \"baseUrl\": \"http://localhost:8080/v1\",\n      \"api\": \"openai-completions\",\n      \"compat\": {\n        \"supportsUsageInStreaming\": false,\n        \"maxTokensField\": \"max_tokens\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n| Campo | Descripción |\n|-------|-------------|\n| `supportsStore` | El proveedor admite el campo `store` |\n| `supportsDeveloperRole` | Utilice el rol `developer` frente a `system` |\n| `supportsReasoningEffort` | Soporte para el parámetro `reasoning_effort` |\n| `supportsUsageInStreaming` | Admite `stream_options: { include_usage: true }` (predeterminado: `true`) |\n| `supportsFinishReason` | Si las respuestas transmitidas incluyen `finish_reason`. Cuando `false`, pi infiere `stop` o `toolUse` cuando finaliza la transmisión. Predeterminado: `true`. |\n| `maxTokensField` | Utilice `max_completion_tokens` o `max_tokens` |\n| `requiresToolResultName` | Incluir `name` en los mensajes de resultados de la herramienta |\n| `requiresAssistantAfterToolResult` | Insertar un mensaje de asistente antes de un mensaje de usuario después de los resultados de la herramienta |\n| `requiresThinkingAsText` | Convierta bloques de pensamiento en texto sin formato |\n| `requiresReasoningContentOnAssistantMessages` | Incluya un `reasoning_content` vacío en todos los mensajes del asistente reproducidos cuando el razonamiento esté habilitado |\n| `thinkingFormat` | Utilice los parámetros de pensamiento `reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template` o `qwen-chat-template` |\n| `chatTemplateKwargs` | `chat_template_kwargs` valores para `thinkingFormat: \"chat-template\"`; use `{ \"$var\": \"thinking.enabled\" }` o `{ \"$var\": \"thinking.effort\" }` para valores de pensamiento controlados por pi |\n| `chatTemplateArgs` | `chat_template_args` valores para `thinkingFormat: \"baseten\"`; use `{ \"$var\": \"thinking.enabled\" }` o `{ \"$var\": \"thinking.effort\" }` para valores de pensamiento controlados por pi |\n| `cacheControlFormat` | Utilice marcadores `cache_control` de estilo antrópico en el mensaje del sistema, la última definición de herramienta y el contenido de texto del último usuario, asistente o resultado de la herramienta. Actualmente solo se admite `anthropic`. |\n| `sendSessionAffinityHeaders` | Para `openai-completions`, envíe encabezados de afinidad de sesión desde la identificación de la sesión cuando el almacenamiento en caché esté habilitado. Predeterminado: `false`. |\n| `sessionAffinityFormat` | Para `openai-completions` y `openai-responses`, el formato de encabezado de afinidad de sesión: `openai` envía `session_id`/`x-client-request-id` (las terminaciones también `x-session-affinity`), `openai-nosession` omite el encabezado `session_id` que contiene guión bajo, `openrouter` envía `x-session-id`. No afecta el parámetro corporal `prompt_cache_key`. Valor predeterminado: detectado automáticamente. |\n| `supportsStrictMode` | Si el proveedor acepta definiciones estrictas de herramientas de función de esquema JSON. Los valores predeterminados dependen del API; Los modelos OpenAI integrados llevan metadatos de capacidad explícitos. |\n| `supportsOpenAIGrammarTools` | Si los API compatibles con OpenAI emiten herramientas gramaticales personalizadas de Lark/regex. Cuando `false`, las herramientas con restricciones gramaticales vuelven a las herramientas de función normal. Predeterminado: `false`; el catálogo de modelos integrado lo habilita para modelos GPT-5+ en OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode y Cloudflare AI Gateway. |\n| `deferredToolsMode` | Utilice la serialización de herramientas diferida específica del proveedor. Actualmente, solo se admite `\"kimi\"` para el formato de finalización de chat compatible con OpenAI de Kimi. |\n| `supportsLongCacheRetention` | Si el proveedor acepta una retención de caché prolongada cuando la retención de caché es `long`: `prompt_cache_retention: \"24h\"` para el almacenamiento en caché de solicitud de OpenAI, o `cache_control.ttl: \"1h\"` cuando `cacheControlFormat` es `anthropic`. Predeterminado: `true`. |\n| `openRouterRouting` | Preferencias de enrutamiento del proveedor OpenRouter. Este objeto se envía tal cual en el campo `provider` del [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |\n| `vercelGatewayRouting` | Configuración de enrutamiento de Vercel AI Gateway para la selección de proveedor (`only`, `order`) |\n\n`openrouter` usa `reasoning: { effort }`. `together` usa `reasoning: { enabled }` y también `reasoning_effort` cuando `supportsReasoningEffort` está habilitado. `qwen` utiliza el nivel superior `enable_thinking`. Utilice `qwen-chat-template` para servidores locales compatibles con Qwen que requieran `chat_template_kwargs.enable_thinking` y `preserve_thinking`. Utilice `chat-template` para plantillas de chat vLLM/Hugging Face que necesitan `chat_template_kwargs` configurables, como `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }` para plantillas de DeepSeek V3.x. Utilice `thinkingFormat: \"baseten\"` con `chatTemplateArgs` para proveedores que exponen controles de alternancia hasta `chat_template_args` y, opcionalmente, admiten el nivel superior `reasoning_effort`.\n\n`cacheControlFormat: \"anthropic\"` es para proveedores compatibles con OpenAI que exponen el almacenamiento en caché de mensajes de estilo Anthropic a través de marcadores `cache_control` en contenido de texto y definiciones de herramientas.\n\nEjemplo:\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"baseUrl\": \"https://openrouter.ai/api/v1\",\n      \"apiKey\": \"$OPENROUTER_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"openrouter/anthropic/claude-3.5-sonnet\",\n          \"name\": \"OpenRouter Claude 3.5 Sonnet\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"allow_fallbacks\": true,\n              \"require_parameters\": false,\n              \"data_collection\": \"deny\",\n              \"zdr\": true,\n              \"enforce_distillable_text\": false,\n              \"order\": [\"anthropic\", \"amazon-bedrock\", \"google-vertex\"],\n              \"only\": [\"anthropic\", \"amazon-bedrock\"],\n              \"ignore\": [\"gmicloud\", \"friendli\"],\n              \"quantizations\": [\"fp16\", \"bf16\"],\n              \"sort\": {\n                \"by\": \"price\",\n                \"partition\": \"model\"\n              },\n              \"max_price\": {\n                \"prompt\": 10,\n                \"completion\": 20\n              },\n              \"preferred_min_throughput\": {\n                \"p50\": 100,\n                \"p90\": 50\n              },\n              \"preferred_max_latency\": {\n                \"p50\": 1,\n                \"p90\": 3,\n                \"p99\": 5\n              }\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```\n\nEjemplo de puerta de enlace AI de Vercel:\n\n```json\n{\n  \"providers\": {\n    \"vercel-ai-gateway\": {\n      \"baseUrl\": \"https://ai-gateway.vercel.sh/v1\",\n      \"apiKey\": \"$AI_GATEWAY_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"moonshotai/kimi-k2.5\",\n          \"name\": \"Kimi K2.5 (Fireworks via Vercel)\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"],\n          \"cost\": { \"input\": 0.6, \"output\": 3, \"cacheRead\": 0, \"cacheWrite\": 0 },\n          \"contextWindow\": 262144,\n          \"maxTokens\": 262144,\n          \"compat\": {\n            \"vercelGatewayRouting\": {\n              \"only\": [\"fireworks\", \"novita\"],\n              \"order\": [\"fireworks\", \"novita\"]\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```","sourceFile":"models.md"},"packages":{"title":"Pi Packages","markdown":"> pi puede ayudarte a crear paquetes pi. Pídale que agrupe sus extensiones, habilidades, prompt templates o temas.\n\n\nLos paquetes Pi incluyen extensiones, habilidades, prompt templates y temas para que puedas compartirlos a través de npm o git. Un paquete puede declarar recursos en `package.json` bajo la clave `pi` o utilizar directorios convencionales.\n\n## Tabla de contenido\n\n- [Install and Manage](#install-and-manage)\n- [Package Sources](#package-sources)\n- [Creating a Pi Package](#creating-a-pi-package)\n- [Package Structure](#package-structure)\n- [Dependencies](#dependencies)\n- [Package Filtering](#package-filtering)\n- [Enable and Disable Resources](#enable-and-disable-resources)\n- [Scope and Deduplication](#scope-and-deduplication)\n\n## Instalar y administrar\n\n> **Seguridad:** Pi los paquetes se ejecutan con acceso completo al sistema. Extensions ejecutar código arbitrario y las habilidades pueden indicarle al modelo que realice cualquier acción, incluida la ejecución de ejecutables. Revise el código fuente antes de instalar paquetes de terceros.\n\n```bash\npi install npm:@foo/bar@1.0.0\npi install git:github.com/user/repo@v1\npi install https://github.com/user/repo  # raw URLs work too\npi install /absolute/path/to/package\npi install ./relative/path/to/package\n\npi remove npm:@foo/bar\npi list                     # show installed packages from settings\npi update                   # update pi only\npi update --all             # update pi, update packages, and reconcile pinned git refs\npi update --extensions      # update packages and reconcile pinned git refs only\npi update --models          # refresh model catalogs only\npi update --self            # update pi only\npi update --self --force    # reinstall pi even if current\npi update npm:@foo/bar      # update one package\npi update --extension npm:@foo/bar\n```\n\nEstos comandos administran paquetes pi y `pi update` pueden actualizar la instalación de pi CLI. Para desinstalar pi, consulte [Quickstart](quickstart.md#uninstall).\n\nDe forma predeterminada, `install` y `remove` escriben en la configuración del usuario (`~/.pi/agent/settings.json`). Utilice `-l` para escribir en la configuración del proyecto (`.pi/settings.json`). La configuración del proyecto se puede compartir con su equipo y pi instala automáticamente los paquetes faltantes al inicio después de que el proyecto sea confiable.\n\nPara probar un paquete sin instalarlo, use `--extension` o `-e`. Esto se instala en un directorio temporal solo para la ejecución actual:\n\n```bash\npi -e npm:@foo/bar\npi -e git:github.com/user/repo\n```\n\n## Fuentes de paquetes\n\nPi acepta tres tipos de fuentes en la configuración y `pi install`.\n\n### npm\n\n```\nnpm:@scope/pkg@1.2.3\nnpm:pkg\n```\n\n- Las especificaciones versionadas se fijan y omiten mediante actualizaciones de paquetes (`pi update --extensions`, `pi update --all`).\n- Las instalaciones de usuario van por debajo de `~/.pi/agent/npm/`.\n- Las instalaciones del proyecto están por debajo de `.pi/npm/`.\n- Establezca `npmCommand` en `settings.json` para anclar npm operaciones de búsqueda e instalación de paquetes a un comando contenedor específico como `mise` o `asdf`.\n\nEjemplo:\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n### git\n\n```\ngit:github.com/user/repo@v1\ngit:git@github.com:user/repo@v1\nhttps://github.com/user/repo@v1\nssh://git@github.com/user/repo@v1\n```\n\n- Sin el prefijo `git:`, solo se aceptan URL de protocolo (`https://`, `http://`, `ssh://`, `git://`).\n- Con el prefijo `git:`, se aceptan formatos abreviados, incluidos `github.com/user/repo` y `git@github.com:user/repo`.\n- Se admiten HTTPS y SSH URL.\n- Las URL SSH utilizan las claves SSH configuradas automáticamente (respeta `~/.ssh/config`).\n- Para ejecuciones no interactivas (por ejemplo, CI), puede configurar `GIT_TERMINAL_PROMPT=0` para deshabilitar las solicitudes de credenciales y configurar `GIT_SSH_COMMAND` (por ejemplo, `ssh -o BatchMode=yes -o ConnectTimeout=5`) para que falle rápidamente.\n- Las referencias son etiquetas fijadas o confirmaciones. `pi update --extensions` y `pi update --all` no los mueven a referencias más nuevas, pero concilian un clon existente con la referencia configurada.\n- Utilice `pi install git:host/user/repo@new-ref` para actualizar la configuración y mover un paquete existente a una nueva referencia fijada.\n- Clonado a `~/.pi/agent/git/<host>/<path>` (global) o `.pi/git/<host>/<path>` (proyecto).\n- Cuando la conciliación cambia el pago, pi restablece y limpia el clon, luego ejecuta `npm install` si `package.json` existe.\n\n**SSH ejemplos:**\n```bash\n# git@host:path shorthand (requires git: prefix)\npi install git:git@github.com:user/repo\n\n# ssh:// protocol format\npi install ssh://git@github.com/user/repo\n\n# With version ref\npi install git:git@github.com:user/repo@v1.0.0\n```\n\n### Caminos locales\n\n```\n/absolute/path/to/package\n./relative/path/to/package\n```\n\nLas rutas locales apuntan a archivos o directorios en el disco y se agregan a la configuración sin copiar. Las rutas relativas se resuelven con respecto al archivo de configuración en el que aparecen. Si la ruta es un archivo, se carga como una única extensión. Si es un directorio, pi carga recursos usando reglas de paquete.\n\n## Creando un paquete Pi\n\nAgregue un manifiesto `pi` a `package.json` o use directorios convencionales. Incluya la palabra clave `pi-package` para mayor visibilidad.\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"skills\": [\"./skills\"],\n    \"prompts\": [\"./prompts\"],\n    \"themes\": [\"./themes\"]\n  }\n}\n```\n\nLas rutas son relativas a la raíz del paquete. Las matrices admiten patrones globales y `!exclusions`.\n\n### Metadatos de la galería\n\nEl [package gallery](https://pi.dev/packages) muestra los paquetes etiquetados con `pi-package`. Agregue los campos `video` o `image` para mostrar una vista previa:\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"video\": \"https://example.com/demo.mp4\",\n    \"image\": \"https://example.com/screenshot.png\"\n  }\n}\n```\n\n- **vídeo**: solo MP4. En el escritorio, se reproduce automáticamente al pasar el mouse. Al hacer clic se abre un reproductor en pantalla completa.\n- **imagen**: PNG, JPEG, GIF o WebP. Se muestra como una vista previa estática.\n\nSi ambos están configurados, el vídeo tiene prioridad.\n\n## Estructura del paquete\n\n### Directorios de convenciones\n\nSi no hay ningún manifiesto `pi` presente, pi descubre automáticamente los recursos de estos directorios:\n\n- `extensions/` carga los archivos `.ts` y `.js`\n- `skills/` busca recursivamente carpetas `SKILL.md` y carga archivos `.md` de nivel superior como habilidades\n- `prompts/` carga `.md` archivos\n- `themes/` carga `.json` archivos\n\n## Dependencias\n\nLas dependencias de tiempo de ejecución de terceros pertenecen a `dependencies` en `package.json`. Las dependencias que no registran extensiones, habilidades, prompt templates o temas también pertenecen a `dependencies`. Cuando pi instala un paquete desde npm o git, ejecuta `npm install`, por lo que esas dependencias se instalan automáticamente.\n\nPi incluye paquetes principales para extensiones y habilidades. Si importa alguno de estos, enumerelos en `peerDependencies` con un rango `\"*\"` y no los agrupe: `@earendil-works/pi-ai`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, `typebox`.\n\nOtros paquetes pi deben estar incluidos en su tarball. Agréguelos a `dependencies` y `bundledDependencies`, luego haga referencia a sus recursos a través de las rutas `node_modules/`. Pi carga paquetes con raíces de módulos separadas, por lo que las instalaciones separadas no chocan ni comparten módulos.\n\nEjemplo:\n\n```json\n{\n  \"dependencies\": {\n    \"shitty-extensions\": \"^1.0.1\"\n  },\n  \"bundledDependencies\": [\"shitty-extensions\"],\n  \"pi\": {\n    \"extensions\": [\"extensions\", \"node_modules/shitty-extensions/extensions\"],\n    \"skills\": [\"skills\", \"node_modules/shitty-extensions/skills\"]\n  }\n}\n```\n\n## Filtrado de paquetes\n\nFiltra lo que carga un paquete usando el formulario de objeto en la configuración:\n\n```json\n{\n  \"packages\": [\n    \"npm:simple-pkg\",\n    {\n      \"source\": \"npm:my-package\",\n      \"extensions\": [\"extensions/*.ts\", \"!extensions/legacy.ts\"],\n      \"skills\": [],\n      \"prompts\": [\"prompts/review.md\"],\n      \"themes\": [\"+themes/legacy.json\"]\n    }\n  ]\n}\n```\n\n`+path` y `-path` son rutas exactas relativas a la raíz del paquete.\n\n- Omita una clave para cargar todos los de ese tipo.\n- Utilice `[]` para no cargar nada de ese tipo.\n- `!pattern` excluye coincidencias.\n- `+path` fuerza: incluye una ruta exacta.\n- `-path` fuerza: excluye una ruta exacta.\n- Capa de filtros encima del manifiesto. Reducen lo que ya está permitido.\n\n## Activar y desactivar recursos\n\nUtilice `pi config` para habilitar o deshabilitar extensiones, habilidades, prompt templates y temas de paquetes instalados y directorios locales. `pi config` comienza en la configuración global (`~/.pi/agent/settings.json`); presione Tab para cambiar entre los modos global y local del proyecto. Utilice `pi config -l` para comenzar en anulaciones de proyectos (`.pi/settings.json`) con los recursos globales heredados atenuados.\n\n## Alcance y deduplicación\n\nLos paquetes pueden aparecer tanto en la configuración global como en la del proyecto. Si el mismo paquete aparece en ambos, la entrada del proyecto gana a menos que la entrada del proyecto tenga `autoload: false`, en cuyo caso se aplica como un delta sobre la entrada global. La identidad está determinada por:\n\n- npm: nombre del paquete\n- git: URL del repositorio sin referencia\n- local: ruta absoluta resuelta","sourceFile":"packages.md"},"prompt-templates":{"title":"Plantillas de mensajes","markdown":"> pi puede crear prompt templates. Pídale que cree uno para su flujo de trabajo.\n\n\nLas plantillas de mensajes son Markdown fragmentos que se expanden hasta formar mensajes completos. Escriba `/name` en el editor para invocar una plantilla, donde `name` es el nombre del archivo sin `.md`.\n\n## Ubicaciones\n\nPi cargas prompt templates desde:\n\n- Global: `~/.pi/agent/prompts/*.md`\n- Proyecto: `.pi/prompts/*.md` (solo después de que se confíe en el proyecto)\n- Paquetes: `prompts/` directorios o `pi.prompts` entradas en `package.json`\n- Configuraciones: `prompts` matriz con archivos o directorios\n- CLI: `--prompt-template <path>` (repetible)\n\nDesactive el descubrimiento con `--no-prompt-templates`.\n\n## Formato\n\n```markdown\n---\ndescription: Review staged git changes\n---\nReview the staged changes (`git diff --cached`). Focus on:\n- Bugs and logic errors\n- Security issues\n- Error handling gaps\n```\n\n- El nombre del archivo se convierte en el nombre del comando. `review.md` se convierte en `/review`.\n- `description` es opcional. Si falta, se utiliza la primera línea que no esté vacía.\n- `argument-hint` es opcional. Cuando se configura, la sugerencia se muestra antes de la descripción en el menú desplegable de autocompletar.\n\n### Sugerencias de argumentos\n\nUtilice `argument-hint` al principio para mostrar los argumentos esperados en autocompletar. Utilice `<angle brackets>` para los argumentos obligatorios y `[square brackets]` para los opcionales:\n\n```markdown\n---\ndescription: Review PRs from URLs with structured issue and code analysis\nargument-hint: \"<PR-URL>\"\n---\n```\n\nEsto se representa en el menú desplegable de autocompletar como:\n\n```\n→ pr   <PR-URL>       — Review PRs from URLs with structured issue and code analysis\n  is   <issue>        — Analyze GitHub issues (bugs or feature requests)\n  wr   [instructions] — Finish the current task end-to-end\n  cl   — Audit changelog entries before release\n```\n\n## Uso\n\nEscriba `/` seguido del nombre de la plantilla en el editor. Autocompletar muestra las plantillas disponibles con descripciones.\n\n```\n/review                           # Expands review.md\n/component Button                 # Expands with argument\n/component Button \"click handler\" # Multiple arguments\n```\n\n## Argumentos\n\nLas plantillas admiten argumentos posicionales, valores predeterminados y cortes simples:\n\n- `$1`, `$2`,... argumentos posicionales\n- `$@` o `$ARGUMENTS` para todos los argumentos unidos\n- `${1:-default}` usa arg 1 cuando está presente/no vacío, de lo contrario `default`\n- `${@:-default}` o `${ARGUMENTS:-default}` usa todos los argumentos cuando están presentes/no vacíos; de lo contrario, `default`\n- `${@:N}` para argumentos desde la enésima posición (indexado en 1)\n- `${@:N:L}` para `L` argumentos comenzando en N\n\nEjemplo:\n\n```markdown\n---\ndescription: Create a component\n---\nCreate a React component named $1 with features: $@\n```\n\nLos valores predeterminados son útiles para argumentos opcionales:\n\n```markdown\nSummarize the current state in ${1:-7} bullet points.\n```\n\nUso: `/component Button \"onClick handler\" \"disabled support\"`\n\n## Cargando reglas\n\n- El descubrimiento de plantillas en `prompts/` no es recursivo.\n- Si desea plantillas en subdirectorios, agréguelas explícitamente mediante la configuración `prompts` o un manifiesto de paquete.","sourceFile":"prompt-templates.md"},"providers":{"title":"Providers","markdown":"Pi admite proveedores basados ​​en suscripción a través de proveedores OAuth y API key a través de variables de entorno o archivos de autenticación. Los catálogos integrados se envían con pi; Los proveedores configurados pueden actualizar los catálogos más nuevos y almacenarlos en caché en `~/.pi/agent/models-store.json` para usarlos sin conexión.\n\n## Tabla de contenido\n\n- [Subscriptions](#subscriptions)\n- [API Keys](#api-keys)\n- [Auth File](#auth-file)\n- [Cloud Providers](#cloud-providers)\n- [llama.cpp](#llamacpp)\n- [Custom Providers](#custom-providers)\n- [Resolution Order](#resolution-order)\n\n## Suscripciones\n\nUtilice `/login` en modo interactivo, luego seleccione un proveedor:\n\n- ChatGPT Plus/Pro (Códex)\n- Claude Pro/Max\n- GitHub Copiloto\n- xAI (suscripción Grok/X)\n- OpenRouter (OAuth- acuñado API key facturado con créditos de OpenRouter)\n- Radio\n\nUtilice `/logout` para borrar las credenciales. Los tokens se almacenan en `~/.pi/agent/auth.json` y se actualizan automáticamente cuando caducan. OpenRouter, en cambio, crea un API key controlado por el usuario que no caduca automáticamente.\n\n### Códice OpenAI\n\n- Requiere suscripción ChatGPT Plus o Pro\n- Respaldado oficialmente por OpenAI: [Codex for OSS](https://developers.openai.com/community/codex-for-oss)\n\n### Claude Pro/Max\n\nLa autenticación de suscripción antrópica está activa para las cuentas Claude Pro/Max. El uso de arneses de terceros comienza desde [extra usage](https://claude.ai/settings/usage) y se factura por token, no según los límites del plan Claude.\n\n### GitHub Copiloto\n\n- Presione Entrar para github.com o ingrese su dominio GitHub Enterprise Server\n- Si aparece \"modelo no compatible\", habilítelo en VS Code: Copilot Chat → selector de modelo → seleccione modelo → \"Habilitar\"\n\n### xAI (suscripción Grok/X)\n\n- Ejecute `/login xai`, luego seleccione **Usar una suscripción**\n- `XAI_API_KEY` permanece disponible a través de **Use un API key**\n\n### enrutador abierto\n\n- Ejecute `/login openrouter`, luego seleccione **Iniciar sesión con OpenRouter** para abrir el flujo de autorización PKCE de OpenRouter\n- La autorización crea un OpenRouter controlado por el usuario API key facturado con sus créditos de OpenRouter\n- En máquinas remotas/sin cabeza (por ejemplo, más de SSH), el navegador no puede acceder a la devolución de llamada en bucle; En su lugar, pegue la URL de redireccionamiento final (o el código de autorización) en el mensaje de inicio de sesión.\n- `OPENROUTER_API_KEY` permanece disponible a través de **Use un API key**\n\n### Radio\n\nRadius es una puerta de enlace dinámica `pi-messages`. `/login radius` almacena OAuth tokens en `auth.json`; el catálogo de la puerta de enlace se actualiza de forma independiente y se almacena en caché en `models-store.json`. Las puertas de enlace Radius personalizadas se pueden declarar en `models.json` con `\"oauth\": \"radius\"` y una puerta de enlace `baseUrl`.\n\n## API Teclas\n\n### Variables de entorno o archivo de autenticación\n\nUtilice `/login` en modo interactivo y seleccione un proveedor para almacenar un API key en `auth.json`, o establezca credenciales mediante una variable de entorno:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\n| Proveedor | Variable de entorno | tecla `auth.json` |\n|----------|----------------------|------------------|\n| antrópico | `ANTHROPIC_API_KEY` | `anthropic` |\n| hormiga ling | `ANT_LING_API_KEY` | `ant-ling` |\n| Respuestas de Azure OpenAI | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |\n| AbiertoAI | `OPENAI_API_KEY` | `openai` |\n| búsqueda profunda | `DEEPSEEK_API_KEY` | `deepseek` |\n| NIM de NVIDIA | `NVIDIA_API_KEY` | `nvidia` |\n| Google Géminis | `GEMINI_API_KEY` | `google` |\n| Roca Amazónica | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` |\n| Mistral | `MISTRAL_API_KEY` | `mistral` |\n| Groq | `GROQ_API_KEY` | `groq` |\n| Cerebras | `CEREBRAS_API_KEY` | `cerebras` |\n| Puerta de enlace de IA de Cloudflare | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_GATEWAY_ID`) | `cloudflare-ai-gateway` |\n| IA de los trabajadores de Cloudflare | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |\n| xAI | `XAI_API_KEY` | `xai` |\n| enrutador abierto | `OPENROUTER_API_KEY` | `openrouter` |\n| Puerta de enlace de IA de Vercel | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |\n| Plan de Codificación ZAI (Global) | `ZAI_API_KEY` | `zai` |\n| Plan de codificación ZAI (China) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` |\n| Código abierto Zen | `OPENCODE_API_KEY` | `opencode` |\n| Código abierto Ir | `OPENCODE_API_KEY` | `opencode-go` |\n| Radio | `RADIUS_API_KEY` | `radius` |\n| Hugging Face | `HF_TOKEN` | `huggingface` |\n| Fuegos artificiales | `FIREWORKS_API_KEY` | `fireworks` |\n| Juntos IA | `TOGETHER_API_KEY` | `together` |\n| Baseten | `BASETEN_API_KEY` | `baseten` |\n| Kimi para codificar | `KIMI_API_KEY` | `kimi-coding` |\n| minimax | `MINIMAX_API_KEY` | `minimax` |\n| MiniMax (China) | `MINIMAX_CN_API_KEY` | `minimax-cn` |\n| Plan Qwen Token (catálogo existente) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` |\n| Plan de tokens Qwen (individual) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan-individual` |\n| Plan de tokens Qwen (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` | `qwen-token-plan-cn` |\n| Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` |\n| Plan de token Xiaomi MiMo (China) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | `xiaomi-token-plan-cn` |\n| Plan de tokens Xiaomi MiMo (Ámsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | `xiaomi-token-plan-ams` |\n| Plan de token Xiaomi MiMo (Singapur) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-sgp` |\n\nReferencia para variables de entorno y claves `auth.json`: [`const envMap`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts) en [`packages/ai/src/env-api-keys.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts).\n\n#### Archivo de autenticación\n\nAlmacene las credenciales en `~/.pi/agent/auth.json`:\n\n```json\n{\n  \"anthropic\": { \"type\": \"api_key\", \"key\": \"sk-ant-...\" },\n  \"ant-ling\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"openai\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"deepseek\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"nvidia\": { \"type\": \"api_key\", \"key\": \"nvapi-...\" },\n  \"google\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode-go\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"together\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"qwen-token-plan\":  { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-individual\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-cn\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"xiaomi\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-cn\":  { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-ams\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-sgp\": { \"type\": \"api_key\", \"key\": \"...\" }\n}\n```\n\n`qwen-token-plan-individual` utiliza el mismo punto final internacional y `QWEN_TOKEN_PLAN_API_KEY` que\n`qwen-token-plan`, pero limita el selector a los modelos documentados para suscripciones individuales. El existente\nEl proveedor mantiene su catálogo más amplio para compatibilidad con versiones anteriores. Cuando utilice `auth.json`, guarde el\ncredencial del proveedor que seleccione; Ambos proveedores internacionales comparten una variable de entorno.\n\nEl archivo se crea con permisos `0600` (solo lectura/escritura del usuario). Las credenciales del archivo de autenticación tienen prioridad sobre las variables de entorno.\n\nAPI key las credenciales también pueden incluir valores de entorno específicos del proveedor. Estos valores se utilizan antes de las variables de entorno del proceso al resolver la clave de credencial, los encabezados de proveedor/modelo y la configuración del proveedor, como los ID de cuenta de Cloudflare, la configuración de Azure OpenAI, el proyecto/ubicación de Vertex, la configuración de Bedrock, `PI_CACHE_RETENTION` y `HTTP_PROXY`/`HTTPS_PROXY`.\n\n```json\n{\n  \"cloudflare-ai-gateway\": {\n    \"type\": \"api_key\",\n    \"key\": \"$CLOUDFLARE_API_KEY\",\n    \"env\": {\n      \"CLOUDFLARE_API_KEY\": \"...\",\n      \"CLOUDFLARE_ACCOUNT_ID\": \"account-id\",\n      \"CLOUDFLARE_GATEWAY_ID\": \"gateway-id\"\n    }\n  }\n}\n```\n\nÚselo cuando pi deba usar configuraciones de proveedor diferentes a las del entorno de shell del proyecto.\n\n### Resolución clave\n\nEl campo `key` admite la ejecución de comandos, la interpolación del entorno y los literales:\n\n- **Comando de Shell:** `\"!command\"` al inicio ejecuta el valor completo como un comando y usa stdout (almacenado en caché durante la vida útil del proceso)\n  ```json\n  { \"type\": \"api_key\", \"key\": \"!security find-generic-password -ws 'anthropic'\" }\n  { \"type\": \"api_key\", \"key\": \"!op read 'op://vault/item/credential'\" }\n  ```\n- **Interpolación del entorno:** `\"$ENV_VAR\"` o `\"${ENV_VAR}\"` usa el valor de la variable nombrada. La interpolación funciona dentro de literales más grandes.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$MY_ANTHROPIC_KEY\" }\n  { \"type\": \"api_key\", \"key\": \"${KEY_PREFIX}_${KEY_SUFFIX}\" }\n  ```\n  `$FOO_BAR` es la variable `FOO_BAR`; use `${FOO}_BAR` cuando `BAR` sea texto literal. Las variables de entorno que faltan hacen que el valor no se resuelva.\n- **Escapa:** `\"$\"` emite un literal `\"$\"`; `\"$!\"` emite un literal `\"!\"` sin activar la ejecución del comando.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$$literal-dollar-prefix\" }\n  { \"type\": \"api_key\", \"key\": \"$!literal-bang-prefix\" }\n  ```\n- **Valor literal:** Usado directamente. Las cadenas simples en mayúsculas como `MY_API_KEY` son literales; utilice `$MY_API_KEY` para las variables de entorno.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"sk-ant-...\" }\n  { \"type\": \"api_key\", \"key\": \"public\" }\n  ```\n\nLas credenciales OAuth también se almacenan aquí después de `/login` y se administran automáticamente.\n\n## Nube Providers\n\n### Azure abierto AI\n\n```bash\nexport AZURE_OPENAI_API_KEY=...\nexport AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com\n# also supported: https://your-resource.cognitiveservices.azure.com\n# also supported: https://your-resource.openai.azure.com\n# root endpoints are auto-normalized to /openai/v1\n# or use resource name instead of base URL\nexport AZURE_OPENAI_RESOURCE_NAME=your-resource\n\n# Optional\nexport AZURE_OPENAI_API_VERSION=2024-02-01\nexport AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-4=my-gpt4,gpt-4o=my-gpt4o\n```\n\n### Roca Amazónica\n\nUtilice `/login amazon-bedrock` para almacenar un Bedrock API key o configure una de las fuentes de credenciales ambientales de AWS a continuación:\n\n```bash\n# Option 1: AWS Profile\nexport AWS_PROFILE=your-profile\n\n# Option 2: IAM Keys\nexport AWS_ACCESS_KEY_ID=AKIA...\nexport AWS_SECRET_ACCESS_KEY=...\n\n# Option 3: Bearer Token\nexport AWS_BEARER_TOKEN_BEDROCK=...\n\n# Optional region (defaults to us-east-1)\nexport AWS_REGION=us-west-2\n```\n\nTambién admite roles de tareas ECS (`AWS_CONTAINER_CREDENTIALS_*`) e IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`).\n\n```bash\npi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0\n```\n\nEl almacenamiento en caché rápido se habilita automáticamente para los modelos Claude cuyo ID contiene un nombre de modelo reconocible (modelos base y perfiles de inferencia definidos por el sistema). Para perfiles de inferencia de aplicaciones (cuyos ARN no contienen el nombre del modelo), configure `AWS_BEDROCK_FORCE_CACHE=1` para habilitar puntos de caché:\n\n```bash\nexport AWS_BEDROCK_FORCE_CACHE=1\npi --provider amazon-bedrock --model arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123\n```\n\nSi se está conectando a un proxy Bedrock API, se pueden utilizar las siguientes variables de entorno:\n\n```bash\n# Set the URL for the Bedrock proxy (standard AWS SDK env var)\nexport AWS_ENDPOINT_URL_BEDROCK_RUNTIME=https://my.corp.proxy/bedrock\n\n# Set if your proxy does not require authentication\nexport AWS_BEDROCK_SKIP_AUTH=1\n\n# Set if your proxy only supports HTTP/1.1\nexport AWS_BEDROCK_FORCE_HTTP1=1\n```\n\n### Puerta de enlace de IA de Cloudflare\n\n`CLOUDFLARE_API_KEY` se puede configurar mediante `/login`. El ID de la cuenta y el slug de la puerta de enlace se pueden configurar como variables de entorno o en el objeto `env` de la credencial API key en `auth.json`.\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\nexport CLOUDFLARE_GATEWAY_ID=...        # create at dash.cloudflare.com → AI → AI Gateway\npi --provider cloudflare-ai-gateway --model \"claude-sonnet-4-5\"\n```\n\nRutas hacia OpenAI, Anthropic y Workers AI a través de Cloudflare AI Gateway. La IA de los trabajadores utiliza el API (`/compat`) unificado y los ID de modelo con prefijo (`workers-ai/@cf/...`). OpenAI utiliza la ruta de paso de OpenAI (`/openai`) con ID de modelo nativo de OpenAI como `gpt-5.1`. Anthropic utiliza la ruta de paso de Anthropic (`/anthropic`) con ID de modelo nativo de Anthropic como `claude-sonnet-4-5`.\n\nLa autenticación AI Gateway utiliza `CLOUDFLARE_API_KEY` como `cf-aig-authorization`. La autenticación ascendente puede ser una de:\n\n| Modo | Solicitar autenticación | autenticación ascendente |\n|------|--------------|---------------|\n| IA de los trabajadores | Solo token de Cloudflare | Nativo de Cloudflare |\n| Facturación unificada | Solo token de Cloudflare | Cloudflare maneja la autenticación ascendente y deduce créditos |\n| BYOK almacenado | Solo token de Cloudflare | Cloudflare inyecta claves de proveedor almacenadas en el panel de AI Gateway |\n| BYOK en línea | Token de Cloudflare más encabezado `Authorization` ascendente | La solicitud proporciona la clave del proveedor ascendente. |\n\nPara un uso normal de pi, prefiera la facturación unificada o BYOK almacenado. BYOK en línea requiere configurar un encabezado `Authorization` ascendente adicional para el proveedor de Cloudflare AI Gateway, por ejemplo, a través de una anulación de proveedor/modelo `models.json`.\n\n### IA de los trabajadores de Cloudflare\n\n`CLOUDFLARE_API_KEY` se puede configurar mediante `/login`. `CLOUDFLARE_ACCOUNT_ID` se puede configurar como una variable de entorno o en el objeto `env` de la credencial API key en `auth.json`.\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\npi --provider cloudflare-workers-ai --model \"@cf/moonshotai/kimi-k2.6\"\n```\n\nPi establece automáticamente `x-session-affinity` para descuentos [prefix caching](https://developers.cloudflare.com/workers-ai/features/prompt-caching/).\n\n### IA de vértice de Google\n\nUtiliza credenciales predeterminadas de la aplicación:\n\n```bash\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=your-project\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\nO establezca `GOOGLE_APPLICATION_CREDENTIALS` en un archivo de clave de cuenta de servicio.\n\n## llama.cpp\n\nPi admite el servidor enrutador llama.cpp. Configúrelo con `/login llama.cpp`, administre los modelos cargados con `/llama` y seleccione un modelo cargado con `/model`.\n\nConsulte [llama.cpp](llama-cpp.md) para conocer la configuración del servidor, el diseño del directorio de modelos, las variables de entorno y el uso de comandos.\n\n## Personalizado Providers\n\n**A través de models.json:** Agregue Ollama, LM Studio, vLLM o cualquier proveedor que hable un API compatible (finalizaciones de OpenAI, respuestas de OpenAI, mensajes antrópicos, IA generativa de Google). Ver [models.md](models.md).\n\n**A través de extensiones:** Para los proveedores que necesitan API implementaciones o OAuth flujos personalizados, cree una extensión. Consulte [custom-provider.md](custom-provider.md) y [examples/extensions/custom-provider-gitlab-duo](../examples/extensions/custom-provider-gitlab-duo/).\n\n## Orden de resolución\n\nAl resolver las credenciales de un proveedor:\n\n1. CLI `--api-key` bandera\n2. `auth.json` entrada (API key o OAuth token)\n3. variable de entorno\n4. Claves de proveedor personalizadas desde `models.json`","sourceFile":"providers.md"},"quickstart":{"title":"Inicio rápido","markdown":"Esta página le lleva desde la instalación hasta una útil primera sesión de pi.\n\n## Instalar\n\nPi se distribuye como un paquete npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` deshabilita los scripts del ciclo de vida de las dependencias durante la instalación. Pi no requiere scripts de instalación para instalaciones normales npm.\n\n### Desinstalar\n\nUtilice el administrador de paquetes que instaló pi. El instalador de curl usa npm globalmente, por lo que las instalaciones de curl y npm se eliminan con npm:\n\n```bash\n# curl installer or npm install -g\nnpm uninstall -g @earendil-works/pi-coding-agent\n\n# pnpm\npnpm remove -g @earendil-works/pi-coding-agent\n\n# Yarn\nyarn global remove @earendil-works/pi-coding-agent\n\n# Bun\nbun uninstall -g @earendil-works/pi-coding-agent\n```\n\nLa desinstalación de pi deja la configuración, las credenciales, las sesiones y los paquetes de pi instalados en `~/.pi/agent/`.\n\nLuego inicie pi en el directorio del proyecto en el que desea que funcione:\n\n```bash\ncd /path/to/project\npi\n```\n\n## Autenticar\n\nPi puede usar proveedores de claves subscription providers hasta `/login`, o API a través de variables de entorno o el archivo de autenticación.\n\n### Opción 1: inicio de sesión de suscripción\n\nInicie pi y ejecute:\n\n```text\n/login\n```\n\nLuego seleccione un proveedor. Los inicios de sesión de suscripción integrados incluyen Claude Pro/Max, ChatGPT Plus/Pro (Codex) y GitHub Copilot.\n\n### Opción 2: API key\n\nEstablece un API key antes de iniciar pi:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\nTambién puede ejecutar `/login` y seleccionar un proveedor de claves API para almacenar la clave en `~/.pi/agent/auth.json`.\n\nConsulte [Providers](providers.md) para conocer todos los proveedores admitidos, las variables de entorno y la configuración del proveedor de la nube.\n\n## Primera sesión\n\nUna vez que se inicie pi, escriba una solicitud y presione Entrar:\n\n```text\nSummarize this repository and tell me how to run its checks.\n```\n\nPor defecto, pi le da al modelo cuatro herramientas:\n\n- `read` - leer archivos\n- `write` - crear o sobrescribir archivos\n- `edit` - archivos de parche\n- `bash` - ejecutar comandos de shell\n\nHerramientas adicionales integradas de solo lectura (`grep`, `find`, `ls`) están disponibles a través de las opciones de herramientas. Pi se ejecuta en su directorio de trabajo actual y puede modificar archivos allí. Utilice git u otro flujo de trabajo de puntos de control si desea una reversión sencilla.\n\n## Dar instrucciones del proyecto pi\n\nPi carga context files al inicio. Agregue un archivo `AGENTS.md` para indicarle cómo trabajar en un proyecto:\n\n```markdown\n# Project Instructions\n\n- Run `npm run check` after code changes.\n- Do not run production migrations locally.\n- Keep responses concise.\n```\n\nPi cargas:\n\n- `~/.pi/agent/AGENTS.md` para instrucciones globales\n- `AGENTS.md` o `CLAUDE.md` de los directorios principales y el directorio actual\n\nSi un directorio contiene `AGENTS.override.md`, Pi lo carga en lugar de `AGENTS.md` o `CLAUDE.md` de ese directorio.\n\nReinicie pi o ejecute `/reload`, después de cambiar context files.\n\n## Cosas comunes para probar\n\n### Archivos de referencia\n\nEscriba `@` en el editor para realizar una búsqueda aproximada de archivos o pase archivos en la línea de comando:\n\n```bash\npi @README.md \"Summarize this\"\npi @src/app.ts @src/app.test.ts \"Review these together\"\n```\n\nSe pueden pegar imágenes o texto con Ctrl+V (Alt+V en Windows); Las imágenes también se pueden arrastrar a terminales compatibles.\n\n### Ejecutar comandos de shell\n\nEn modo interactivo:\n\n```text\n!npm run lint\n```\n\nLa salida del comando se envía al modelo. Utilice `!!command` para ejecutar un comando sin agregar su salida al contexto del modelo.\n\n### Cambiar modelos\n\nUtilice `/model` o Ctrl+L para elegir un modelo. Utilice Shift+Tab para recorrer el nivel de pensamiento. Utilice Ctrl+P / Shift+Ctrl+P para recorrer los modelos con alcance.\n\n### Continuar más tarde\n\nLas sesiones se guardan automáticamente:\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse previous sessions\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Open a specific session\n```\n\nDentro de pi, use `/resume`, `/new`, `/tree`, `/fork` y `/clone` para administrar sesiones.\n\n### Modo no interactivo\n\nPara indicaciones de una sola vez:\n\n```bash\npi -p \"Summarize this codebase\"\ncat README.md | pi -p \"Summarize this text\"\npi -p @screenshot.png \"What's in this image?\"\n```\n\nUtilice `--mode json` para JSON salida de evento o `--mode rpc` para integración de procesos.\n\n## Próximos pasos\n\n- [Using Pi](usage.md) - modo interactivo, slash commands, sesiones, context files y CLI referencia.\n- [Providers](providers.md) - autenticación y configuración del modelo.\n- [Settings](settings.md) - configuración global y de proyecto.\n- [Keybindings](keybindings.md) - atajos y personalización.\n- [Pi Packages](packages.md): instala extensiones, habilidades, indicaciones y temas compartidos.\n\nNotas de plataforma: [Windows](windows.md), [Termux](termux.md), [tmux](tmux.md), [Terminal setup](terminal-setup.md), [Shell aliases](shell-aliases.md).","sourceFile":"quickstart.md"},"rpc":{"title":"RPC Modo","markdown":"El modo RPC permite el funcionamiento sin cabeza del agente de codificación a través de un protocolo JSON sobre stdin/stdout. Esto resulta útil para integrar el agente en otras aplicaciones, IDE o UI personalizadas.\n\n**Nota para usuarios de Node.js/TypeScript**: si está creando una aplicación Node.js, considere usar `AgentSession` directamente desde `@earendil-works/pi-coding-agent` en lugar de generar un subproceso. Vea [`src/core/agent-session.ts`](../src/core/agent-session.ts) para el API. Para un cliente TypeScript basado en subprocesos, consulte [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts).\n\n## Iniciando modo RPC\n\n```bash\npi --mode rpc [options]\n```\n\nOpciones comunes:\n- `--provider <name>`: establece el proveedor de LLM (anthropic, openai, google, etc.)\n- `--model <pattern>`: Patrón de modelo o ID (admite `provider/id` y opcional `:<thinking>`)\n- `--name <name>` / `-n <name>`: establece el nombre para mostrar de la sesión al inicio\n- `--no-session`: Desactivar la persistencia de la sesión\n- `--session-dir <path>`: Directorio de almacenamiento de sesión personalizado\n\n## Descripción general del protocolo\n\n- **Comandos**: JSON objetos enviados a stdin, uno por línea\n- **Respuestas**: JSON objetos con `type: \"response\"` que indican el éxito/fracaso del comando\n- **Eventos**: los eventos del agente se transmiten a stdout como JSON líneas\n\nTodos los comandos admiten un campo `id` opcional para la correlación de solicitud/respuesta. Si se proporciona, la respuesta correspondiente incluirá el mismo `id`. Los eventos `bash_execution_update` también incluyen el `id` de su comando `bash` de origen.\n\n### Enmarcado\n\nEl modo RPC utiliza una semántica JSONL estricta con LF (`\\n`) como único delimitador de registros.\n\nEsto es importante para los clientes:\n- Dividir registros solo en `\\n`\n- Acepte la entrada `\\r\\n` opcional eliminando un `\\r` final\n- No utilice lectores de líneas genéricos que traten los separadores Unicode como nuevas líneas.\n\nEn particular, el nodo `readline` no cumple con el protocolo para el modo RPC porque también se divide en `U+2028` y `U+2029`, que son válidos dentro de las cadenas JSON.\n\n## Comandos\n\n### Incitación\n\n#### inmediato\n\nEnvíe un mensaje de usuario al agente. La respuesta del comando se emite después de aceptar, poner en cola o manejar el mensaje. Los eventos continúan transmitiéndose de forma asincrónica después de la aceptación.\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\nCon imágenes:\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**Durante la transmisión**: si el agente ya está transmitiendo, debe especificar `streamingBehavior` para poner en cola el mensaje:\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`: Poner en cola el mensaje mientras el agente se está ejecutando. Se entrega después de que el turno actual del asistente termina de ejecutar sus llamadas a herramientas, antes de la siguiente llamada LLM.\n- `\"followUp\"`: Espere hasta que termine el agente. El mensaje se entrega solo cuando el agente se detiene.\n\nSi el agente está transmitiendo y no se especifica ningún `streamingBehavior`, el comando devuelve un error.\n\n**Comandos de extensión**: si el mensaje es un comando de extensión (por ejemplo, `/mycommand`), se ejecuta inmediatamente incluso durante la transmisión. Los comandos de extensión gestionan su propia interacción LLM a través de `pi.sendMessage()`.\n\n**Expansión de entrada**: los comandos de habilidad (`/skill:name`) y prompt templates (`/template`) se expanden antes de enviar/poner en cola.\n\nRespuesta:\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true}\n```\n\n`success: true` significa que el mensaje fue aceptado, puesto en cola o manejado inmediatamente. `success: false` significa que la solicitud fue rechazada antes de ser aceptada. Las fallas después de la aceptación se informan a través del evento normal y el flujo de mensajes, no como un segundo `response` para la misma identificación de solicitud.\n\nEl campo `images` es opcional. Cada imagen utiliza el formato `ImageContent`: `{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`.\n\n#### buey\n\nPonga en cola un mensaje de dirección mientras el agente se está ejecutando. Se entrega después de que el turno actual del asistente termina de ejecutar sus llamadas a herramientas, antes de la siguiente llamada LLM. Los comandos de habilidad y prompt templates se amplían. Los comandos de extensión no están permitidos (use `prompt` en su lugar).\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\nCon imágenes:\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nEl campo `images` es opcional. Cada imagen utiliza el formato `ImageContent` (igual que `prompt`).\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true}\n```\n\nConsulte [set_steering_mode](#set_steering_mode) para controlar cómo se procesan los mensajes de dirección.\n\n#### hacer un seguimiento\n\nPonga en cola un mensaje de seguimiento para que se procese una vez que finalice el agente. Se entrega solo cuando el agente no tiene más llamadas de herramientas ni mensajes de dirección. Los comandos de habilidad y prompt templates se amplían. Los comandos de extensión no están permitidos (use `prompt` en su lugar).\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\nCon imágenes:\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nEl campo `images` es opcional. Cada imagen utiliza el formato `ImageContent` (igual que `prompt`).\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true}\n```\n\nConsulte [set_follow_up_mode](#set_follow_up_mode) para controlar cómo se procesan los mensajes de seguimiento.\n\n#### abortar\n\nCancele la operación del agente actual.\n\n```json\n{\"type\": \"abort\"}\n```\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n#### nueva_sesión\n\nInicie una nueva sesión. Puede ser cancelado por un controlador de eventos de extensión `session_before_switch`.\n\n```json\n{\"type\": \"new_session\"}\n```\n\nCon seguimiento opcional de la sesión de los padres:\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nSi se cancela una extensión:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### Estado\n\n#### obtener_estado\n\nObtener el estado actual de la sesión.\n\n```json\n{\"type\": \"get_state\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_state\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isStreaming\": false,\n    \"isCompacting\": false,\n    \"steeringMode\": \"all\",\n    \"followUpMode\": \"one-at-a-time\",\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"sessionName\": \"my-feature-work\",\n    \"autoCompactionEnabled\": true,\n    \"messageCount\": 5,\n    \"pendingMessageCount\": 0\n  }\n}\n```\n\nEl campo `model` es un objeto [Model](#model) completo o `null`. El campo `sessionName` es el nombre para mostrar establecido mediante `set_session_name`, o se omite si no está configurado.\n\n#### obtener_mensajes\n\nRecibe todos los mensajes de la conversación.\n\n```json\n{\"type\": \"get_messages\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\nLos mensajes son `AgentMessage` objetos (ver [Message Types](#message-types)).\n\n### Modelo\n\n#### establecer_modelo\n\nCambie a un modelo específico.\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\nLa respuesta contiene el objeto [Model](#model) completo:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n#### modelo_ciclo\n\nPasa al siguiente modelo disponible. Devuelve datos `null` si solo hay un modelo disponible.\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_model\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isScoped\": false\n  }\n}\n```\n\nEl campo `model` es un objeto [Model](#model) completo.\n\n#### obtener_modelos_disponibles\n\nEnumere todos los modelos configurados.\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\nLa respuesta contiene una serie de objetos [Model](#model) completos:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n### Pensamiento\n\n#### establecer_nivel_de_pensamiento\n\nEstablezca el nivel de razonamiento/pensamiento para los modelos que lo respaldan.\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\nNiveles: `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"`\n\n`\"xhigh\"` y `\"max\"` están expuestos solo cuando son compatibles con el modelo seleccionado. Algunos modelos, incluido el GPT-5.6, exponen ambos.\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n#### nivel_de_pensamiento_ciclo\n\nRecorra los niveles de pensamiento disponibles. Devuelve datos `null` si el modelo no admite el pensamiento.\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n#### get_available_thinking_levels\n\nEnumere los niveles de pensamiento respaldados por el modelo actual. Devuelve `[\"off\"]` para un modelo sin soporte de razonamiento.\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_thinking_levels\",\n  \"success\": true,\n  \"data\": {\n    \"levels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"]\n  }\n}\n```\n\n### Modos de cola\n\n#### establecer_modo_dirección\n\nControle cómo se entregan los mensajes de dirección (desde `steer`).\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModos:\n- `\"all\"`: entrega todos los mensajes de dirección después de que el turno del asistente actual termine de ejecutar sus llamadas a herramientas\n- `\"one-at-a-time\"`: envía un mensaje de dirección por cada turno completado del asistente (predeterminado)\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n#### set_follow_up_mode\n\nControle cómo se entregan los mensajes de seguimiento (desde `follow_up`).\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModos:\n- `\"all\"`: entregar todos los mensajes de seguimiento cuando el agente finalice\n- `\"one-at-a-time\"`: Entregar un mensaje de seguimiento por finalización del agente (predeterminado)\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n### Compactación\n\n#### compacto\n\nContexto de conversación compacto manualmente para reducir el uso de tokens.\n\n```json\n{\"type\": \"compact\"}\n```\n\nCon instrucciones personalizadas:\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"compact\",\n  \"success\": true,\n  \"data\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  }\n}\n```\n\n`estimatedTokensAfter` es una estimación heurística sobre el contexto del mensaje reconstruido inmediatamente después de la compactación, no un recuento de tokens exacto del proveedor. `usage` informa la llamada o llamadas de LLM que generaron el resumen y los controladores de compactación personalizados pueden omitirlo.\n\n#### set_auto_compactation\n\nHabilite o deshabilite la compactación automática cuando el contexto esté casi lleno.\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n### Rever\n\n#### set_auto_retry\n\nHabilite o deshabilite el reintento automático en errores transitorios (sobrecarga, límite de velocidad, 5xx).\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n#### abortar_reintentar\n\nCancelar un reintento en curso (cancelar el retraso y detener el reintento).\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n### Intento\n\n#### bash\n\nEjecute un comando de shell y agregue resultados al contexto de la conversación. La salida se transmite como eventos `bash_execution_update` mientras se ejecuta el comando; la respuesta contiene el resultado final.\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\nIncluya un `id` para asociar eventos `bash_execution_update` transmitidos con este comando.\n\nRespuesta:\n```json\n{\n  \"id\": \"req-1\",\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"total 48\\ndrwxr-xr-x ...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": false\n  }\n}\n```\n\nSi la salida se truncó, incluye `fullOutputPath`:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"truncated output...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": true,\n    \"fullOutputPath\": \"/tmp/pi-bash-abc123.log\"\n  }\n}\n```\n\n**Cómo llegan los resultados bash al LLM:**\n\nEl comando `bash` se ejecuta inmediatamente y devuelve un `BashResult`. Internamente, se crea un `BashExecutionMessage` y se almacena en el estado del mensaje del agente.\n\nCuando se envía el siguiente comando `prompt`, todos los mensajes (incluido `BashExecutionMessage`) se transforman antes de enviarse al LLM. El `BashExecutionMessage` se convierte en un `UserMessage` con este formato:\n\n````\nRan `ls -la`\n```\ntotal 48\ndrwxr-xr-x...\n```\n````\n\nEsto significa:\n1. La salida de Bash se incluye en el contexto LLM en el **siguiente mensaje**, no inmediatamente\n2. Se pueden ejecutar varios comandos bash antes de un mensaje; todas las salidas serán incluidas\n\n#### abortar_bash\n\nCancelar un comando bash en ejecución.\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n### Sesión\n\n#### get_session_stats\n\nObtenga el uso de tokens, estadísticas de costos y uso de la ventana de contexto actual.\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_session_stats\",\n  \"success\": true,\n  \"data\": {\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"userMessages\": 5,\n    \"assistantMessages\": 5,\n    \"toolCalls\": 12,\n    \"toolResults\": 12,\n    \"totalMessages\": 22,\n    \"tokens\": {\n      \"input\": 50000,\n      \"output\": 10000,\n      \"cacheRead\": 40000,\n      \"cacheWrite\": 5000,\n      \"total\": 105000\n    },\n    \"cost\": 0.45,\n    \"contextUsage\": {\n      \"tokens\": 60000,\n      \"contextWindow\": 200000,\n      \"percent\": 30\n    }\n  }\n}\n```\n\n`tokens` y `cost` incluyen mensajes del asistente, uso informado por herramientas y generación de resumen de compactación/rama durante toda la sesión. `contextUsage` contiene la estimación actual de la ventana de contexto utilizada para la compactación y la visualización del pie de página.\n\n`contextUsage` se omite cuando no hay ningún modelo o ventana de contexto disponible. `contextUsage.tokens` y `contextUsage.percent` son `null` inmediatamente después de la compactación hasta que una nueva respuesta del asistente posterior a la compactación proporcione datos de uso válidos.\n\n#### exportar_html\n\nExportar sesión a un archivo HTML.\n\n```json\n{\"type\": \"export_html\"}\n```\n\nCon ruta personalizada:\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n#### cambiar_sesión\n\nCargue un archivo de sesión diferente. Puede ser cancelado por un controlador de eventos de extensión `session_before_switch`.\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\nRespuesta:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nSi una extensión canceló el cambio:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n#### tenedor\n\nCree una nueva bifurcación a partir de un mensaje de usuario anterior en la rama activa. Puede ser cancelado por un controlador de eventos de extensión `session_before_fork`. Devuelve el texto del mensaje del que se bifurca.\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\nSi una extensión canceló la bifurcación:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": true}\n}\n```\n\n#### clon\n\nDuplica la rama activa actual en una nueva sesión en la posición actual. Puede ser cancelado por un controlador de eventos de extensión `session_before_fork`.\n\n```json\n{\"type\": \"clone\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\nSi una extensión canceló el clon:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n#### get_fork_messages\n\nObtenga mensajes de usuario disponibles para bifurcar.\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_fork_messages\",\n  \"success\": true,\n  \"data\": {\n    \"messages\": [\n      {\"entryId\": \"abc123\", \"text\": \"First prompt...\"},\n      {\"entryId\": \"def456\", \"text\": \"Second prompt...\"}\n    ]\n  }\n}\n```\n\n#### obtener_entradas\n\nObtenga todas las entradas de la sesión en orden de adición (excluyendo el encabezado de la sesión). La sesión es un árbol de entradas de solo adición con identificadores estables, por lo que un identificador de entrada funciona como un cursor duradero: pase el último identificador de entrada que vio como `since` para obtener solo entradas estrictamente posteriores, incluso durante los reinicios del cliente. A diferencia de `get_messages`, esto incluye el historial de precompactación y las ramas abandonadas.\n\n```json\n{\"type\": \"get_entries\"}\n```\n\nCon un cursor:\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_entries\",\n  \"success\": true,\n  \"data\": {\n    \"entries\": [\n      {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"timestamp\": \"...\", \"message\": {\"role\": \"user\", \"...\": \"...\"}}\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n`leafId` es la identificación de la entrada de hoja actual (`null` para una sesión vacía), por lo que un cliente puede saber en un viaje de ida y vuelta si la rama activa se movió. Si `since` no coincide con ningún ID de entrada, la respuesta es `success: false`.\n\n#### obtener_arbol\n\nObtenga la sesión como un árbol de entradas. Cada nodo es `{entry, children, label?, labelTimestamp?}`. Una sesión bien formada tiene una única raíz; Las entradas huérfanas (cadena principal rota) también aparecen como raíces.\n\n```json\n{\"type\": \"get_tree\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_tree\",\n  \"success\": true,\n  \"data\": {\n    \"tree\": [\n      {\n        \"entry\": {\"type\": \"message\", \"id\": \"abc123\", \"parentId\": null, \"...\": \"...\"},\n        \"children\": [\n          {\"entry\": {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"...\": \"...\"}, \"children\": []}\n        ]\n      }\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n#### get_last_assistant_text\n\nObtenga el contenido de texto del último mensaje del asistente.\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_last_assistant_text\",\n  \"success\": true,\n  \"data\": {\"text\": \"The assistant's response...\"}\n}\n```\n\nDevuelve `{\"text\": null}` si no existen mensajes del asistente.\n\n#### establecer_nombre_sesión\n\nEstablezca un nombre para mostrar para la sesión actual. El nombre aparece en los listados de sesiones y ayuda a identificar las sesiones.\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\nEl nombre de la sesión actual está disponible a través de `get_state` en el campo `sessionName`. Para establecer el nombre inicial al iniciar el modo RPC, pase `--name <name>` o `-n <name>` al proceso `pi --mode rpc`.\n\n### Comandos\n\n#### obtener_comandos\n\nObtenga comandos disponibles (comandos de extensión, prompt templates y habilidades). Estos se pueden invocar mediante el comando `prompt` con el prefijo `/`.\n\n```json\n{\"type\": \"get_commands\"}\n```\n\nRespuesta:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_commands\",\n  \"success\": true,\n  \"data\": {\n    \"commands\": [\n      {\"name\": \"session-name\", \"description\": \"Set or clear session name\", \"source\": \"extension\", \"path\": \"/home/user/.pi/agent/extensions/session.ts\"},\n      {\"name\": \"fix-tests\", \"description\": \"Fix failing tests\", \"source\": \"prompt\", \"location\": \"project\", \"path\": \"/home/user/myproject/.pi/agent/prompts/fix-tests.md\"},\n      {\"name\": \"skill:brave-search\", \"description\": \"Web search via Brave API\", \"source\": \"skill\", \"location\": \"user\", \"path\": \"/home/user/.pi/agent/skills/brave-search/SKILL.md\"}\n    ]\n  }\n}\n```\n\nCada comando tiene:\n- `name`: Nombre del comando (invocar con `/name`)\n- `description`: descripción legible por humanos (opcional para comandos de extensión)\n- `source`: Qué tipo de comando:\n  - `\"extension\"`: Registrado vía `pi.registerCommand()` en una extensión\n  - `\"prompt\"`: Cargado desde un archivo de plantilla de aviso `.md`\n  - `\"skill\"`: Cargado desde un directorio de habilidades (el nombre tiene el prefijo `skill:`)\n- `location`: Desde dónde se cargó (opcional, no presente para extensiones):\n  - `\"user\"`: nivel de usuario (`~/.pi/agent/`)\n  - `\"project\"`: Nivel de proyecto (`./.pi/agent/`)\n  - `\"path\"`: ruta explícita a través de CLI o configuración\n- `path`: Ruta absoluta del archivo a la fuente del comando (opcional)\n\n**Nota**: Los comandos TUI integrados (`/settings`, `/hotkeys`, etc.) no están incluidos. Se manejan únicamente en modo interactivo y no se ejecutarían si se enviaran a través de `prompt`.\n\n## Eventos\n\nLos eventos se transmiten a stdout como JSON líneas durante la operación del agente. Los eventos generalmente no incluyen un campo `id`; `bash_execution_update` incluye el `id` de su comando `bash` de origen cuando se proporcionó uno.\n\n### Tipos de eventos\n\n| Evento | Descripción |\n|-------|-------------|\n| `agent_start` | El agente comienza a procesar |\n| `agent_end` | Se completa una ejecución del agente de bajo nivel (aún puede ir seguida de reintentos, compactación o continuaciones en cola) |\n| `agent_settled` | La ejecución del agente está completamente liquidada; no queda ningún reintento automático, reintento de compactación ni continuación en cola |\n| `turn_start` | Comienza un nuevo turno |\n| `turn_end` | Turno completado (incluye mensaje del asistente y resultados de la herramienta) |\n| `message_start` | Comienza el mensaje |\n| `message_update` | Actualización de transmisión (deltas de texto/pensamiento/llamada a herramientas) |\n| `message_end` | Mensaje completo |\n| `bash_execution_update` | Fragmento de salida del comando directo RPC bash |\n| `tool_execution_start` | La herramienta comienza a ejecutarse |\n| `tool_execution_update` | Progreso de ejecución de la herramienta (salida de transmisión) |\n| `tool_execution_end` | Herramienta completa |\n| `queue_update` | Cambio de cola de dirección/seguimiento pendiente |\n| `compaction_start` | Comienza la compactación |\n| `compaction_end` | La compactación se completa |\n| `auto_retry_start` | Comienza el reintento automático (después de un error transitorio) |\n| `auto_retry_end` | El reintento automático se completa (éxito o fracaso final) |\n| `summarization_retry_scheduled` | Reintento programado para un error de resumen de resumen de rama o compactación transitoria |\n| `summarization_retry_attempt_start` | Se inicia la solicitud de resumen reintentada |\n| `summarization_retry_finished` | Se completa el ciclo de reintento de resumen |\n| `extension_error` | La extensión arrojó un error |\n\n### inicio_agente\n\nSe emite cuando el agente comienza a procesar un mensaje.\n\n```json\n{\"type\": \"agent_start\"}\n```\n\n### agente_end\n\nSe emite cuando se completa la ejecución de un agente de bajo nivel. Contiene todos los mensajes generados durante esta ejecución. Si `willRetry` es verdadero, se realizará un reintento automático.\n\n```json\n{\n  \"type\": \"agent_end\",\n  \"messages\": [...],\n  \"willRetry\": false\n}\n```\n\n### agente_resuelto\n\nEmitido después de que se establece la ejecución completa del nivel de sesión. En este punto, Pi no continuará automáticamente mediante el reintento, el reintento de compactación ni los mensajes de seguimiento en cola.\n\n```json\n{\"type\": \"agent_settled\"}\n```\n\n### turn_start / turn_end\n\nUn turno consta de la respuesta de un asistente más las llamadas y resultados de las herramientas resultantes.\n\n```json\n{\"type\": \"turn_start\"}\n```\n\n```json\n{\n  \"type\": \"turn_end\",\n  \"message\": {...},\n  \"toolResults\": [...]\n}\n```\n\n### inicio_mensaje / fin_mensaje\n\nSe emite cuando un mensaje comienza y finaliza. El campo `message` contiene un `AgentMessage`.\n\n```json\n{\"type\": \"message_start\", \"message\": {...}}\n{\"type\": \"message_end\", \"message\": {...}}\n```\n\n### message_update (Transmisión)\n\nEmitido durante la transmisión de mensajes del asistente. Contiene un evento delta sin una instantánea de mensaje acumulativo.\n\n```json\n{\n  \"type\": \"message_update\",\n  \"assistantMessageEvent\": {\n    \"type\": \"text_delta\",\n    \"contentIndex\": 0,\n    \"delta\": \"Hello \"\n  }\n}\n```\n\nEl campo `assistantMessageEvent` contiene uno de estos tipos delta:\n\n| Tipo | Descripción |\n|------|-------------|\n| `text_start` | Bloque de contenido de texto iniciado |\n| `text_delta` | Fragmento de contenido de texto |\n| `text_end` | Bloque de contenido de texto finalizado |\n| `thinking_start` | Bloqueo de pensamiento iniciado. |\n| `thinking_delta` | Fragmento de contenido de pensamiento |\n| `thinking_end` | El bloque de pensamiento terminó |\n| `toolcall_start` | Llamada de herramienta iniciada |\n| `toolcall_delta` | Fragmento de argumentos de llamada de herramienta |\n| `toolcall_end` | La llamada a la herramienta finalizó (incluye el objeto `toolCall` completo) |\n\nEjemplo de transmisión de una respuesta de texto:\n```json\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_start\",\"contentIndex\":0}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\" world\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_end\",\"contentIndex\":0,\"content\":\"Hello world\"}}\n```\n\n`message_update` omite intencionalmente el antiguo campo acumulativo `message` y\n`assistantMessageEvent.partial`. Los clientes que necesiten un mensaje parcial en vivo deben armarlo.\ndesde `message_start` y eventos posteriores usando `contentIndex`. Tratar `message_end.message`\ncomo autoritario. Para llamadas a herramientas, buffer `toolcall_delta.delta`; `toolcall_end.toolCall`\ncontiene la llamada completada.\n\n### bash_execution_update\n\nEmitido una vez por cada fragmento de salida de un comando `bash` directo. `id` coincide con el `id` del comando, lo que permite a los clientes asociar la salida con el comando correcto.\n\nLos eventos transmiten todos los resultados mientras se ejecuta el comando, incluso si la respuesta `bash` final `output` está truncada.\n\n```json\n{\n  \"type\": \"bash_execution_update\",\n  \"id\": \"req-1\",\n  \"delta\": \"total 48\\n\"\n}\n```\n\n### inicio_ejecución_herramienta / actualización_ejecución_herramienta / fin_ejecución_herramienta\n\nSe emite cuando una herramienta comienza, transmite el progreso y completa la ejecución.\n\n```json\n{\n  \"type\": \"tool_execution_start\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"}\n}\n```\n\nDurante la ejecución, los eventos `tool_execution_update` transmiten resultados parciales (p. ej., salida bash a medida que llega):\n\n```json\n{\n  \"type\": \"tool_execution_update\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"},\n  \"partialResult\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"partial output so far...\"}],\n    \"details\": {\"truncation\": null, \"fullOutputPath\": null}\n  }\n}\n```\n\nCuando esté completo:\n\n```json\n{\n  \"type\": \"tool_execution_end\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"result\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"total 48\\n...\"}],\n    \"details\": {...}\n  },\n  \"isError\": false\n}\n```\n\nUtilice `toolCallId` para correlacionar eventos. El `partialResult` en `tool_execution_update` contiene la salida acumulada hasta el momento (no solo el delta), lo que permite a los clientes simplemente reemplazar su pantalla en cada actualización.\n\n### actualización_cola\n\nSe emite cada vez que cambia la dirección pendiente o la cola de seguimiento.\n\n```json\n{\n  \"type\": \"queue_update\",\n  \"steering\": [\"Focus on error handling\"],\n  \"followUp\": [\"After that, summarize the result\"]\n}\n```\n\n### inicio_compactación / fin_compactación\n\nEmitido cuando se ejecuta la compactación, ya sea manual o automática.\n\n```json\n{\"type\": \"compaction_start\", \"reason\": \"threshold\"}\n```\n\nEl campo `reason` es `\"manual\"`, `\"threshold\"` o `\"overflow\"`.\n\n```json\n{\n  \"type\": \"compaction_end\",\n  \"reason\": \"threshold\",\n  \"result\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  },\n  \"aborted\": false,\n  \"willRetry\": false\n}\n```\n\nSi `reason` era `\"overflow\"` y la compactación se realiza correctamente, `willRetry` es `true` y el agente volverá a intentarlo automáticamente.\n\nSi se canceló la compactación, `result` es `null` y `aborted` es `true`.\n\nSi la compactación falló (por ejemplo, se superó la cuota API), `result` es `null`, `aborted` es `false` y `errorMessage` contiene la descripción del error.\n\n### auto_retry_start / auto_retry_end\n\nSe emite cuando se activa el reintento automático después de un error transitorio (sobrecarga, límite de velocidad, 5xx).\n\n```json\n{\n  \"type\": \"auto_retry_start\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"529 {\\\"type\\\":\\\"error\\\",\\\"error\\\":{\\\"type\\\":\\\"overloaded_error\\\",\\\"message\\\":\\\"Overloaded\\\"}}\"\n}\n```\n\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": true,\n  \"attempt\": 2\n}\n```\n\nEn caso de fallo final (se superó el número máximo de reintentos):\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": false,\n  \"attempt\": 3,\n  \"finalError\": \"529 overloaded_error: Overloaded\"\n}\n```\n\n### reintento_summarización_programado / reintento_summarización_inicio_inicio / reintento_summarización_terminado\n\nSe emite cuando se reintenta la compactación o el resumen de resumen de ramas después de un error transitorio del proveedor. Estos eventos utilizan la misma configuración de reintento que los reintentos automáticos de turno del asistente.\n\n```json\n{\n  \"type\": \"summarization_retry_scheduled\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"terminated\"\n}\n```\n\n```json\n{\n  \"type\": \"summarization_retry_attempt_start\",\n  \"source\": \"compaction\",\n  \"reason\": \"threshold\"\n}\n```\n\nPara resúmenes de sucursales, `source` es `\"branchSummary\"` y no hay `reason` presente.\n\n```json\n{\n  \"type\": \"summarization_retry_finished\"\n}\n```\n\n### error_extensión\n\nEmitido cuando una extensión arroja un error.\n\n```json\n{\n  \"type\": \"extension_error\",\n  \"extensionPath\": \"/path/to/extension.ts\",\n  \"event\": \"tool_call\",\n  \"error\": \"Error message...\"\n}\n```\n\n## Protocolo de interfaz de usuario de extensión\n\nExtensions puede solicitar la interacción del usuario a través de `ctx.ui.select()`, `ctx.ui.confirm()`, etc. En el modo RPC, estos se traducen en un subprotocolo de solicitud/respuesta en la parte superior del flujo de comando/evento base.\n\nHay dos categorías de métodos de interfaz de usuario de extensión:\n\n- **Métodos de diálogo** (`select`, `confirm`, `input`, `editor`): emite un `extension_ui_request` en stdout y bloquea hasta que el cliente devuelva un `extension_ui_response` en stdin con el `id` correspondiente.\n- **Métodos de disparar y olvidar** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): emite un `extension_ui_request` en stdout pero no esperes una respuesta. El cliente puede mostrar la información o ignorarla.\n\nSi un método de diálogo incluye un campo `timeout`, el lado del agente se resolverá automáticamente con un valor predeterminado cuando expire el tiempo de espera. El cliente no necesita realizar un seguimiento de los tiempos de espera.\n\nAlgunos métodos `ExtensionUIContext` no son compatibles o están degradados en el modo RPC porque requieren acceso directo TUI:\n- `custom()` devuelve `undefined`\n- `setWorkingMessage()`, `setWorkingIndicator()`, `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()` no son operaciones\n- `getEditorText()` devuelve `\"\"`\n- `getToolsExpanded()` devuelve `false`\n- `pasteToEditor()` delega a `setEditorText()` (sin manejo de pegado/colapso)\n- `getAllThemes()` devuelve `[]`\n- `getTheme()` devuelve `undefined`\n- `setTheme()` devuelve `{ success: false, error: \"...\" }`\n\nNota: `ctx.mode` es `\"rpc\"` y `ctx.hasUI` es `true` en el modo RPC porque el diálogo y los métodos de disparar y olvidar funcionan a través del subprotocolo de la interfaz de usuario de extensión. Utilice `ctx.mode === \"tui\"` para proteger funciones específicas de TUI como `custom()` que requieren una terminal real.\n\n### Solicitudes de interfaz de usuario de extensión (stdout)\n\nTodas las solicitudes tienen `type: \"extension_ui_request\"`, un campo `id` único y un campo `method`.\n\n#### seleccionar\n\nSolicite al usuario que elija de una lista. Los métodos de diálogo con un campo `timeout` incluyen el tiempo de espera en milisegundos; el agente se resuelve automáticamente con `undefined` si el cliente no responde a tiempo.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-1\",\n  \"method\": \"select\",\n  \"title\": \"Allow dangerous command?\",\n  \"options\": [\"Allow\", \"Block\"],\n  \"timeout\": 10000\n}\n```\n\nRespuesta esperada: `extension_ui_response` con `value` (la cadena de opción seleccionada) o `cancelled: true`.\n\n#### confirmar\n\nSolicite al usuario una confirmación de sí/no.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-2\",\n  \"method\": \"confirm\",\n  \"title\": \"Clear session?\",\n  \"message\": \"All messages will be lost.\",\n  \"timeout\": 5000\n}\n```\n\nRespuesta esperada: `extension_ui_response` con `confirmed: true/false` o `cancelled: true`.\n\n#### aporte\n\nSolicite al usuario texto de formato libre.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-3\",\n  \"method\": \"input\",\n  \"title\": \"Enter a value\",\n  \"placeholder\": \"type something...\"\n}\n```\n\nRespuesta esperada: `extension_ui_response` con `value` (el texto ingresado) o `cancelled: true`.\n\n#### editor\n\nAbra un editor de texto de varias líneas con contenido precargado opcional.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-4\",\n  \"method\": \"editor\",\n  \"title\": \"Edit some text\",\n  \"prefill\": \"Line 1\\nLine 2\\nLine 3\"\n}\n```\n\nRespuesta esperada: `extension_ui_response` con `value` (el texto editado) o `cancelled: true`.\n\n#### notificar\n\nMostrar una notificación. Dispara y olvida, no se espera respuesta.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-5\",\n  \"method\": \"notify\",\n  \"message\": \"Command blocked by user\",\n  \"notifyType\": \"warning\"\n}\n```\n\nEl campo `notifyType` es `\"info\"`, `\"warning\"` o `\"error\"`. El valor predeterminado es `\"info\"` si se omite.\n\n#### establecer estado\n\nEstablezca o borre una entrada de estado en el pie de página/barra de estado. Dispara y olvida.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-6\",\n  \"method\": \"setStatus\",\n  \"statusKey\": \"my-ext\",\n  \"statusText\": \"Turn 3 running...\"\n}\n```\n\nEnvíe `statusText: undefined` (u omítalo) para borrar la entrada de estado de esa clave.\n\n#### establecerWidget\n\nConfigure o borre un widget (bloque de líneas de texto) que se muestra encima o debajo del editor. Dispara y olvida.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-7\",\n  \"method\": \"setWidget\",\n  \"widgetKey\": \"my-ext\",\n  \"widgetLines\": [\"--- My Widget ---\", \"Line 1\", \"Line 2\"],\n  \"widgetPlacement\": \"aboveEditor\"\n}\n```\n\nEnvíe `widgetLines: undefined` (u omítalo) para borrar el widget. El campo `widgetPlacement` es `\"aboveEditor\"` (predeterminado) o `\"belowEditor\"`. Solo se admiten matrices de cadenas en el modo RPC; Se ignoran las fábricas de componentes.\n\n#### establecer título\n\nEstablezca el título de la ventana/pestaña del terminal. Dispara y olvida.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-8\",\n  \"method\": \"setTitle\",\n  \"title\": \"pi - my project\"\n}\n```\n\n#### establecer_editor_texto\n\nConfigure el texto en el editor de entrada. Dispara y olvida.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-9\",\n  \"method\": \"set_editor_text\",\n  \"text\": \"prefilled text for the user\"\n}\n```\n\n### Respuestas de la interfaz de usuario de extensión (stdin)\n\nLas respuestas se envían únicamente para los métodos de diálogo (`select`, `confirm`, `input`, `editor`). El `id` debe coincidir con la solicitud.\n\n#### Respuesta de valor (selección, entrada, editor)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n#### Respuesta de confirmación (confirmar)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n#### Respuesta de cancelación (cualquier diálogo)\n\nDescarta cualquier método de diálogo. La extensión recibe `undefined` (para seleccionar/entrada/editor) o `false` (para confirmar).\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## Manejo de errores\n\nLos comandos fallidos devuelven una respuesta con `success: false`:\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": false,\n  \"error\": \"Model not found: invalid/model\"\n}\n```\n\nErrores de análisis:\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"parse\",\n  \"success\": false,\n  \"error\": \"Failed to parse command: Unexpected token...\"\n}\n```\n\n## Tipos\n\nArchivos fuente:\n- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `Model`, `UserMessage`, `AssistantMessage`, `ToolResultMessage`\n- [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `AgentEvent`\n- [`src/core/messages.ts`](../src/core/messages.ts) - `BashExecutionMessage`\n- [`src/modes/json-event.ts`](../src/modes/json-event.ts) - `JsonAgentSessionEvent`\n- [`src/modes/rpc/rpc-types.ts`](../src/modes/rpc/rpc-types.ts) - RPC tipos de comando/respuesta, tipos de solicitud/respuesta de interfaz de usuario de extensión\n\n### Modelo\n\n```json\n{\n  \"id\": \"claude-sonnet-4-20250514\",\n  \"name\": \"Claude Sonnet 4\",\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"baseUrl\": \"https://api.anthropic.com\",\n  \"reasoning\": true,\n  \"input\": [\"text\", \"image\"],\n  \"contextWindow\": 200000,\n  \"maxTokens\": 16384,\n  \"cost\": {\n    \"input\": 3.0,\n    \"output\": 15.0,\n    \"cacheRead\": 0.3,\n    \"cacheWrite\": 3.75\n  }\n}\n```\n\n### Mensaje de usuario\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": \"Hello!\",\n  \"timestamp\": 1733234567890,\n  \"attachments\": []\n}\n```\n\nEl campo `content` puede ser una cadena o una matriz de bloques `TextContent`/`ImageContent`.\n\n### Mensaje del asistente\n\n```json\n{\n  \"role\": \"assistant\",\n  \"content\": [\n    {\"type\": \"text\", \"text\": \"Hello! How can I help?\"},\n    {\"type\": \"thinking\", \"thinking\": \"User is greeting me...\"},\n    {\"type\": \"toolCall\", \"id\": \"call_123\", \"name\": \"bash\", \"arguments\": {\"command\": \"ls\"}}\n  ],\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"model\": \"claude-sonnet-4-20250514\",\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"stopReason\": \"stop\",\n  \"timestamp\": 1733234567890\n}\n```\n\nMotivos de parada: `\"stop\"`, `\"length\"`, `\"toolUse\"`, `\"error\"`, `\"aborted\"`\n\n### HerramientaResultadoMensaje\n\n```json\n{\n  \"role\": \"toolResult\",\n  \"toolCallId\": \"call_123\",\n  \"toolName\": \"bash\",\n  \"content\": [{\"type\": \"text\", \"text\": \"total 48\\ndrwxr-xr-x ...\"}],\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"totalTokens\": 150,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"isError\": false,\n  \"timestamp\": 1733234567890\n}\n```\n\n`usage` es opcional e informa el trabajo de LLM anidado realizado por la herramienta. Cuando está presente, contribuye al token de sesión y a los costos totales.\n\n### Mensaje de ejecución de Bash\n\nCreado por el comando `bash` RPC (no mediante llamadas a la herramienta LLM):\n\n```json\n{\n  \"role\": \"bashExecution\",\n  \"command\": \"ls -la\",\n  \"output\": \"total 48\\ndrwxr-xr-x ...\",\n  \"exitCode\": 0,\n  \"cancelled\": false,\n  \"truncated\": false,\n  \"fullOutputPath\": null,\n  \"timestamp\": 1733234567890\n}\n```\n\n### Adjunto\n\n```json\n{\n  \"id\": \"img1\",\n  \"type\": \"image\",\n  \"fileName\": \"photo.jpg\",\n  \"mimeType\": \"image/jpeg\",\n  \"size\": 102400,\n  \"content\": \"base64-encoded-data...\",\n  \"extractedText\": null,\n  \"preview\": null\n}\n```\n\n## Ejemplo: Cliente básico (Python)\n\n```python\nimport subprocess\nimport json\n\nproc = subprocess.Popen(\n    [\"pi\", \"--mode\", \"rpc\", \"--no-session\"],\n    stdin=subprocess.PIPE,\n    stdout=subprocess.PIPE,\n    text=True\n)\n\ndef send(cmd):\n    proc.stdin.write(json.dumps(cmd) + \"\\n\")\n    proc.stdin.flush()\n\ndef read_events():\n    for line in proc.stdout:\n        yield json.loads(line)\n\n# Send prompt\nsend({\"type\": \"prompt\", \"message\": \"Hello!\"})\n\n# Process events\nfor event in read_events():\n    if event.get(\"type\") == \"message_update\":\n        delta = event.get(\"assistantMessageEvent\", {})\n        if delta.get(\"type\") == \"text_delta\":\n            print(delta[\"delta\"], end=\"\", flush=True)\n    \n    if event.get(\"type\") == \"agent_end\":\n        print()\n        break\n```\n\n## Ejemplo: Cliente interactivo (Node.js)\n\nConsulte [`test/rpc-example.ts`](../test/rpc-example.ts) para ver un ejemplo interactivo completo o [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts) para ver una implementación de cliente escrita.\n\nPara ver un ejemplo completo de cómo manejar el protocolo UI de extensión, consulte [`examples/rpc-extension-ui.ts`](../examples/rpc-extension-ui.ts), que se combina con la extensión [`examples/extensions/rpc-demo.ts`](../examples/extensions/rpc-demo.ts).\n\n```javascript\nconst { spawn } = require(\"child_process\");\nconst { StringDecoder } = require(\"string_decoder\");\n\nconst agent = spawn(\"pi\", [\"--mode\", \"rpc\", \"--no-session\"]);\n\nfunction attachJsonlReader(stream, onLine) {\n    const decoder = new StringDecoder(\"utf8\");\n    let buffer = \"\";\n\n    stream.on(\"data\", (chunk) => {\n        buffer += typeof chunk === \"string\" ? chunk : decoder.write(chunk);\n\n        while (true) {\n            const newlineIndex = buffer.indexOf(\"\\n\");\n            if (newlineIndex === -1) break;\n\n            let line = buffer.slice(0, newlineIndex);\n            buffer = buffer.slice(newlineIndex + 1);\n            if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\n            onLine(line);\n        }\n    });\n\n    stream.on(\"end\", () => {\n        buffer += decoder.end();\n        if (buffer.length > 0) {\n            onLine(buffer.endsWith(\"\\r\") ? buffer.slice(0, -1) : buffer);\n        }\n    });\n}\n\nattachJsonlReader(agent.stdout, (line) => {\n    const event = JSON.parse(line);\n\n    if (event.type === \"message_update\") {\n        const { assistantMessageEvent } = event;\n        if (assistantMessageEvent.type === \"text_delta\") {\n            process.stdout.write(assistantMessageEvent.delta);\n        }\n    }\n});\n\n// Send prompt\nagent.stdin.write(JSON.stringify({ type: \"prompt\", message: \"Hello\" }) + \"\\n\");\n\n// Abort on Ctrl+C\nprocess.on(\"SIGINT\", () => {\n    agent.stdin.write(JSON.stringify({ type: \"abort\" }) + \"\\n\");\n});\n```","sourceFile":"rpc.md"},"sdk":{"title":"SDK","markdown":"> pi puede ayudarte a usar el SDK. Pídale que cree una integración para su caso de uso.\n\n\nEl SDK proporciona acceso programático a las capacidades del agente de pi. Úselo para integrar pi en otras aplicaciones, crear interfaces personalizadas o integrarlo con flujos de trabajo automatizados.\n\n**Casos de uso de ejemplo:**\n- Cree una interfaz de usuario personalizada (web, escritorio, móvil)\n- Integre las capacidades del agente en las aplicaciones existentes\n- Cree canales automatizados con razonamiento de agentes\n- Cree herramientas personalizadas que generen subagentes\n- Probar el comportamiento del agente mediante programación\n\nConsulte [examples/sdk/](../examples/sdk/) para ver ejemplos de trabajo desde control mínimo hasta control total.\n\n## Inicio rápido\n\n```typescript\nimport { createAgentSession, ModelRuntime, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n  modelRuntime,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"What files are in the current directory?\");\n```\n\n## Instalación\n\n```bash\nnpm install @earendil-works/pi-coding-agent\n```\n\nEl SDK está incluido en el paquete principal. No se necesita instalación separada.\n\n## Conceptos básicos\n\n### crear sesión de agente()\n\nLa función principal de fábrica para un solo `AgentSession`.\n\n`createAgentSession()` usa un `ResourceLoader` para proporcionar extensiones, habilidades, prompt templates, temas y context files. Si no proporciona uno, utiliza `DefaultResourceLoader` con descubrimiento estándar.\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Minimal: defaults with DefaultResourceLoader\nconst { session } = await createAgentSession();\n\n// Custom: override specific options\nconst { session } = await createAgentSession({\n  model: myModel,\n  tools: [\"read\", \"bash\"],\n  sessionManager: SessionManager.inMemory(),\n});\n```\n\n### Sesión de agente\n\nLa sesión gestiona el ciclo de vida del agente, el historial de mensajes, el estado del modelo, la compactación y la transmisión de eventos.\n\n```typescript\ninterface AgentSession {\n  // Send a prompt and wait for completion\n  prompt(text: string, options?: PromptOptions): Promise<void>;\n\n  // Queue messages during streaming\n  steer(text: string): Promise<void>;\n  followUp(text: string): Promise<void>;\n\n  // Subscribe to events (returns unsubscribe function)\n  subscribe(listener: (event: AgentSessionEvent) => void): () => void;\n\n  // Session info\n  sessionFile: string | undefined;\n  sessionId: string;\n\n  // Model control\n  setModel(model: Model): Promise<void>;\n  setThinkingLevel(level: ThinkingLevel): void;\n  cycleModel(): Promise<ModelCycleResult | undefined>;\n  cycleThinkingLevel(): ThinkingLevel | undefined;\n\n  // State access\n  agent: Agent;\n  model: Model | undefined;\n  thinkingLevel: ThinkingLevel;\n  messages: AgentMessage[];\n  isStreaming: boolean;\n\n  // In-place tree navigation within the current session file\n  navigateTree(targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string }): Promise<{ editorText?: string; cancelled: boolean }>;\n\n  // Compaction\n  compact(customInstructions?: string): Promise<CompactionResult>;\n  abortCompaction(): void;\n\n  // Abort current operation\n  abort(): Promise<void>;\n\n  // Cleanup\n  dispose(): void;\n}\n```\n\nReemplazo de sesión API, como nueva sesión, reanudar, bifurcar e importar, se activa en `AgentSessionRuntime`, no en `AgentSession`.\n\n### crearAgentSessionRuntime() y AgentSessionRuntime\n\nUtilice el tiempo de ejecución API cuando necesite reemplazar la sesión activa y reconstruir el estado del tiempo de ejecución vinculado a cwd.\nEsta es la misma capa que utilizan los modos integrados interactivo, de impresión y RPC.\n\n`createAgentSessionRuntime()` toma una fábrica de tiempo de ejecución más el objetivo inicial de sesión/cwd. La fábrica cierra las entradas fijas del proceso global, recrea los servicios vinculados a cwd para el cwd efectivo, resuelve las opciones de sesión contra esos servicios y devuelve un resultado de tiempo de ejecución completo.\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n```\n\n`AgentSessionRuntime` posee el reemplazo del tiempo de ejecución activo en:\n\n- `newSession()`\n- `switchSession()`\n- `fork()`\n- clon fluye a través de `fork(entryId, { position: \"at\" })`\n- `importFromJsonl()`\n\nComportamiento importante:\n\n- `runtime.session` cambios después de esas operaciones\n- las suscripciones a eventos están adjuntas a un `AgentSession` específico, así que vuelva a suscribirse después del reemplazo\n- si usa extensiones, llame nuevamente al `runtime.session.bindExtensions(...)` para la nueva sesión\n- la creación devuelve diagnósticos en `runtime.diagnostics`\n- Si falla la creación o el reemplazo del tiempo de ejecución, el método se lanza y la persona que llama decide cómo manejarlo.\n\n```typescript\nlet session = runtime.session;\nlet unsubscribe = session.subscribe(() => {});\n\nawait runtime.newSession();\n\nunsubscribe();\nsession = runtime.session;\nunsubscribe = session.subscribe(() => {});\n```\n\n### Solicitudes y colas de mensajes\n\n`PromptOptions` controla la expansión de avisos, el comportamiento de cola durante la transmisión y las notificaciones de verificación previa:\n\n```typescript\ninterface PromptOptions {\n  expandPromptTemplates?: boolean;\n  images?: ImageContent[];\n  streamingBehavior?: \"steer\" | \"followUp\";\n  source?: InputSource;\n  preflightResult?: (success: boolean) => void;\n}\n```\n\n`preflightResult` se llama una vez por cada `prompt()` invocación:\n\n- `true` cuando el mensaje fue aceptado, puesto en cola o manejado inmediatamente\n- `false` cuando se rechaza la verificación previa antes de la aceptación\n\nSe dispara antes de que se resuelva `prompt()`. `prompt()` todavía se resuelve solo después de que finaliza la ejecución completa aceptada, incluidos los reintentos. Los errores después de la aceptación se informan a través del flujo normal de eventos y mensajes, no a través de `preflightResult(false)`.\n\nEl método `prompt()` maneja prompt templates, comandos de extensión y envío de mensajes:\n\n```typescript\n// Basic prompt (when not streaming)\nawait session.prompt(\"What files are here?\");\n\n// With images\nawait session.prompt(\"What's in this image?\", {\n  images: [{ type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } }]\n});\n\n// During streaming: must specify how to queue the message\nawait session.prompt(\"Stop and do this instead\", { streamingBehavior: \"steer\" });\nawait session.prompt(\"After you're done, also check X\", { streamingBehavior: \"followUp\" });\n```\n\n**Comportamiento:**\n- **Comandos de extensión** (por ejemplo, `/mycommand`): se ejecutan inmediatamente, incluso durante la transmisión. Gestionan su propia interacción LLM a través de `pi.sendMessage()`.\n- **Basado en archivos prompt templates** (de archivos `.md`): ampliado a su contenido antes de enviarlos o ponerlos en cola.\n- **Durante la transmisión sin `streamingBehavior`**: arroja un error. Utilice `steer()` o `followUp()` directamente, o especifique la opción.\n- **`preflightResult(true)`**: Significa que el mensaje fue aceptado, puesto en cola o manejado inmediatamente.\n- **`preflightResult(false)`**: Significa verificación previa rechazada antes de la aceptación.\n\nPara colas explícitas durante la transmisión:\n\n```typescript\n// Queue a steering message for delivery after the current assistant turn finishes its tool calls\nawait session.steer(\"New instruction\");\n\n// Wait for agent to finish (delivered only when agent stops)\nawait session.followUp(\"After you're done, also do this\");\n```\n\nTanto `steer()` como `followUp()` expanden prompt templates según archivos, pero se produce un error en los comandos de extensión (los comandos de extensión no se pueden poner en cola).\n\n### Agente y EstadoAgente\n\nLa clase `Agent` (de `@earendil-works/pi-agent-core`) maneja la interacción principal de LLM. Accede a través de `session.agent`.\n\n```typescript\n// Access current state\nconst state = session.agent.state;\n\n// state.messages: AgentMessage[] - conversation history\n// state.model: Model - current model\n// state.thinkingLevel: ThinkingLevel - current thinking level\n// state.systemPrompt: string - system prompt\n// state.tools: AgentTool[] - available tools\n// state.streamingMessage?: AgentMessage - current partial assistant message\n// state.errorMessage?: string - latest assistant error\n\n// Replace messages (useful for branching or restoration)\nsession.agent.state.messages = messages; // copies the top-level array\n\n// Replace tools\nsession.agent.state.tools = tools; // copies the top-level array\n\n// Wait for agent to finish processing\nawait session.agent.waitForIdle();\n```\n\n### Eventos\n\nSuscríbase a eventos para recibir resultados de transmisión y notificaciones del ciclo de vida.\n\n```typescript\nsession.subscribe((event) => {\n  switch (event.type) {\n    // Streaming text from assistant\n    case \"message_update\":\n      if (event.assistantMessageEvent.type === \"text_delta\") {\n        process.stdout.write(event.assistantMessageEvent.delta);\n      }\n      if (event.assistantMessageEvent.type === \"thinking_delta\") {\n        // Thinking output (if thinking enabled)\n      }\n      break;\n    \n    // Tool execution\n    case \"tool_execution_start\":\n      console.log(`Tool: ${event.toolName}`);\n      break;\n    case \"tool_execution_update\":\n      // Streaming tool output\n      break;\n    case \"tool_execution_end\":\n      console.log(`Result: ${event.isError ? \"error\" : \"success\"}`);\n      break;\n    \n    // Message lifecycle\n    case \"message_start\":\n      // New message starting\n      break;\n    case \"message_end\":\n      // Message complete\n      break;\n    \n    // Agent lifecycle\n    case \"agent_start\":\n      // Agent started processing prompt\n      break;\n    case \"agent_end\":\n      // Agent finished (event.messages contains new messages)\n      break;\n    \n    // Turn lifecycle (one LLM response + tool calls)\n    case \"turn_start\":\n      break;\n    case \"turn_end\":\n      // event.message: assistant response\n      // event.toolResults: tool results from this turn\n      break;\n    \n    // Session events (queue, compaction, retry)\n    case \"queue_update\":\n      console.log(event.steering, event.followUp);\n      break;\n    case \"compaction_start\":\n    case \"compaction_end\":\n    case \"auto_retry_start\":\n    case \"auto_retry_end\":\n    case \"summarization_retry_scheduled\":\n    case \"summarization_retry_attempt_start\":\n    case \"summarization_retry_finished\":\n      break;\n  }\n});\n```\n\n## Referencia de opciones\n\n### Directorios\n\n```typescript\nconst { session } = await createAgentSession({\n  // Working directory for DefaultResourceLoader discovery\n  cwd: process.cwd(), // default\n  \n  // Global config directory\n  agentDir: \"~/.pi/agent\", // default (expands ~)\n});\n```\n\n`cwd` es utilizado por `DefaultResourceLoader` para:\n- Extensiones de proyecto (`.pi/extensions/`)\n- Habilidades de proyecto:\n  - `.pi/skills/`\n  - `.agents/skills/` en `cwd` y directorios ancestrales (hasta la raíz del repositorio de git o la raíz del sistema de archivos cuando no está en un repositorio)\n- Indicaciones del proyecto (`.pi/prompts/`)\n- Archivos de contexto (`AGENTS.md` subiendo desde cwd)\n- Nomenclatura del directorio de sesiones\n\n`agentDir` es utilizado por `DefaultResourceLoader` para:\n- Extensiones globales (`extensions/`)\n- Habilidades globales:\n  - `skills/` debajo de `agentDir` (por ejemplo `~/.pi/agent/skills/`)\n  - `~/.agents/skills/`\n- Avisos globales (`prompts/`)\n- Archivo de contexto global (`AGENTS.md`)\n- Configuración (`settings.json`)\n- Modelos personalizados (`models.json`)\n- Credenciales (`auth.json`)\n- Sesiones (`sessions/`)\n\nCuando pasa un `ResourceLoader` personalizado, `cwd` y `agentDir` ya no controlan el descubrimiento de recursos. Todavía influyen en la denominación de las sesiones y la resolución de la ruta de la herramienta.\n\n### Modelo\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\n\n// Find specific built-in model (doesn't check if API key exists)\nconst opus = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!opus) throw new Error(\"Model not found\");\n\n// Find any model by provider/id, including custom models from models.json\n// (doesn't check if API key exists)\nconst customModel = modelRuntime.getModel(\"my-provider\", \"my-model\");\n\n// Get only models that have valid authentication configured\nconst available = await modelRuntime.getAvailable();\n\nconst { session } = await createAgentSession({\n  model: opus,\n  thinkingLevel: \"medium\", // off, minimal, low, medium, high, xhigh, max\n  \n  // Models for cycling (Ctrl+P in interactive mode)\n  scopedModels: [\n    { model: opus, thinkingLevel: \"high\" },\n    { model: haiku, thinkingLevel: \"off\" },\n  ],\n  \n  modelRuntime,\n});\n```\n\nSi no se proporciona ningún modelo:\n1. Intenta restaurar desde la sesión (si continúa)\n2. Utiliza la configuración predeterminada\n3. Vuelve al primer modelo disponible\n\nPara hacer coincidir el análisis del modelo CLI, utilice los ayudantes de resolución exportados:\n\n```typescript\nimport {\n  resolveCliModel,\n  resolveModelScopeWithDiagnostics,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst cliModel = resolveCliModel({\n  cliModel: \"anthropic/claude-opus-4-5:high\",\n  modelRuntime,\n});\nif (cliModel.error) throw new Error(cliModel.error);\nif (cliModel.warning) console.warn(cliModel.warning);\n\nconst { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics(\n  [\"anthropic/*:high\", \"gpt-5\"],\n  modelRuntime,\n);\nfor (const diagnostic of diagnostics) {\n  console.warn(diagnostic.message);\n}\n```\n\n`resolveCliModel()` utiliza todos los modelos registrados, por lo que la configuración inicial del estilo `--api-key` puede resolver un modelo antes de que exista la autenticación almacenada. `resolveModelScopeWithDiagnostics()` coincide con la semántica `--models` y `enabledModels` y devuelve advertencias en lugar de imprimirlas.\n\n> Ver [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)\n\n### API Teclas y OAuth\n\nPrioridad de resolución de autenticación (manejada por `ModelRuntime`):\n1. Anulaciones de tiempo de ejecución (a través de `setRuntimeApiKey`, no persistentes)\n2. Credenciales almacenadas en `auth.json` (API keys o OAuth tokens)\n3. Variables de entorno (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)\n4. Resolución alternativa (para claves de proveedor personalizadas de `models.json`)\n\n```typescript\nimport { InMemoryCredentialStore } from \"@earendil-works/pi-ai\";\nimport { createAgentSession, ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\n// Default: uses ~/.pi/agent/auth.json and ~/.pi/agent/models.json\nconst modelRuntime = await ModelRuntime.create();\n\n// Provider-owned auth methods and current status\nfor (const provider of modelRuntime.getProviders()) {\n  const status = await modelRuntime.checkAuth(provider.id);\n  console.log(provider.name, provider.auth, status);\n}\n\n// Runtime API key override (not persisted to disk)\nawait modelRuntime.setRuntimeApiKey(\"anthropic\", \"sk-my-temp-key\");\n\n// Custom credential and model locations\nconst customRuntime = await ModelRuntime.create({\n  authPath: \"/my/app/auth.json\",\n  modelsPath: \"/my/app/models.json\",\n});\n\n// Or inject any pi-ai CredentialStore\nconst credentials = new InMemoryCredentialStore();\nconst inMemoryRuntime = await ModelRuntime.create({ credentials });\n\nconst { session } = await createAgentSession({\n  modelRuntime: customRuntime,\n});\n```\n\n`login()`, `logout()`, `setRuntimeApiKey()` y `removeRuntimeApiKey()` se resuelven después de que el catálogo integrado/en caché, la composición y la instantánea de disponibilidad del proveedor afectado sean coherentes localmente. No esperan la actualización remota del catálogo. Si se confirmaron las credenciales pero falla la sincronización local, se rechazan con el `CredentialSynchronizationError` exportado; inspeccione sus campos `providerId`, `operation`, `credential` y `cause` en lugar de volver a intentar la mutación de credenciales a ciegas.\n\nLas operaciones públicas de modelo/autenticación y `ModelRuntime.create({ signal })` aceptan señales de cancelación opcionales y son ilimitadas cuando se omiten. SDK Política de plazos propios de las aplicaciones para la actualización remota del catálogo:\n\n```typescript\nconst signal = AbortSignal.timeout(15_000);\nconst result = await modelRuntime.refresh({\n  providers: [\"anthropic\"],\n  signal,\n});\nif (result.aborted) console.warn(\"Catalog refresh timed out; using cached models\");\nfor (const [providerId, error] of result.errors) {\n  console.warn(`Could not refresh ${providerId}:`, error);\n}\n```\n\nUna actualización de red fallida o con tiempo de espera agotado no deshace una operación de credencial exitosa. `refresh()` inicia una nueva generación de proveedores, por lo que no espera detrás de una actualización anterior estancada y las generaciones obsoletas no pueden publicar después.\n\n> Ver [examples/sdk/09-api-keys-and-oauth.ts](../examples/sdk/09-api-keys-and-oauth.ts)\n\n### Aviso del sistema\n\nUtilice un `ResourceLoader` para anular el mensaje del sistema:\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  systemPromptOverride: () => \"You are a helpful assistant.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> Ver [examples/sdk/03-custom-prompt.ts](../examples/sdk/03-custom-prompt.ts)\n\n### Herramientas\n\nEspecifique qué herramientas integradas habilitar:\n\n- Nombres de herramientas integradas: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`\n- Integrados predeterminados: `read`, `bash`, `edit`, `write`\n- `noTools: \"all\"` desactiva todas las herramientas\n- `noTools: \"builtin\"` deshabilita las funciones integradas predeterminadas mientras mantiene habilitadas las extensiones y las herramientas personalizadas\n- `excludeTools` deshabilita nombres específicos de herramientas integradas, de extensión o personalizadas después de aplicar cualquier lista de permitidos `tools`\n\nLa herramienta `edit` devuelve `details.diff` para la pantalla TUI de Pi y `details.patch` como un parche unificado estándar para los consumidores SDK.\n\n```typescript\nimport { createAgentSession } from \"@earendil-works/pi-coding-agent\";\n\n// Read-only mode\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"grep\", \"find\", \"ls\"],\n});\n\n// Pick specific tools\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"bash\", \"grep\"],\n});\n\n// Disable one tool while keeping the rest available\nconst { session } = await createAgentSession({\n  excludeTools: [\"ask_question\"],\n});\n```\n\n#### Herramientas con cwd personalizado\n\nCuando pasas un `cwd` personalizado, `createAgentSession()` crea herramientas integradas seleccionadas para ese cwd.\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst cwd = \"/path/to/project\";\n\n// Use default tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  sessionManager: SessionManager.inMemory(cwd),\n});\n\n// Or pick specific tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  tools: [\"read\", \"bash\", \"grep\"],\n  sessionManager: SessionManager.inMemory(cwd),\n});\n```\n\n> Ver [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Herramientas personalizadas\n\n```typescript\nimport { Type } from \"typebox\";\nimport { createAgentSession, defineTool } from \"@earendil-works/pi-coding-agent\";\n\n// Inline custom tool\nconst myTool = defineTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Does something useful\",\n  parameters: Type.Object({\n    input: Type.String({ description: \"Input value\" }),\n  }),\n  execute: async (_toolCallId, params) => ({\n    content: [{ type: \"text\", text: `Result: ${params.input}` }],\n    details: {},\n  }),\n});\n\n// Pass custom tools directly\nconst { session } = await createAgentSession({\n  customTools: [myTool],\n});\n```\n\nUtilice `defineTool()` para definiciones y matrices independientes como `customTools: [myTool]`. Inline `pi.registerTool({... })` ya infiere los tipos de parámetros correctamente.\n\nLas herramientas personalizadas pasadas a través de `customTools` se combinan con herramientas registradas en extensión. Extensions cargado por ResourceLoader también puede registrar herramientas a través de `pi.registerTool()`.\n\nSi pasa `tools`, incluya cada nombre de herramienta personalizada o de extensión que desee habilitar, por ejemplo `tools: [\"read\", \"bash\", \"my_tool\"]`.\n\n> Ver [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Extensions\n\nExtensions se cargan con el `ResourceLoader`. `DefaultResourceLoader` descubre extensiones de `~/.pi/agent/extensions/`, `.pi/extensions/` y fuentes de extensión settings.json.\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  additionalExtensionPaths: [\"/path/to/my-extension.ts\"],\n  extensionFactories: [\n    (pi) => {\n      pi.on(\"agent_start\", () => {\n        console.log(\"[Inline Extension] Agent starting\");\n      });\n    },\n  ],\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\nExtensions puede registrar herramientas, suscribirse a eventos, agregar comandos y más. Consulte [extensions.md](extensions.md) para ver el API completo.\n\n**Extensiones en línea con nombre:** De forma predeterminada, las fábricas en línea se muestran como `<inline:1>`, `<inline:2>`, etc. en la lista de inicio Extensions. Para mostrar un nombre descriptivo, ajuste la fábrica:\n\n```typescript\nimport type { InlineExtension } from \"@earendil-works/pi-coding-agent\";\n\nconst myProvider: InlineExtension = {\n  name: \"my-provider\",\n  factory: (pi) => {\n    pi.on(\"agent_start\", () => {\n      console.log(\"[my-provider] Agent starting\");\n    });\n  },\n};\n\nconst loader = new DefaultResourceLoader({\n  extensionFactories: [myProvider],\n});\n```\n\nEsto se muestra como `<inline:my-provider>` en lugar de `<inline:1>`. Las funciones básicas de fábrica todavía se aceptan por compatibilidad con versiones anteriores.\n\n**Bus de eventos:** Extensions puede comunicarse a través de `pi.events`. Pasa un `eventBus` compartido al `DefaultResourceLoader` si necesitas emitir o escuchar desde el exterior:\n\n```typescript\nimport { createEventBus, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst eventBus = createEventBus();\nconst loader = new DefaultResourceLoader({\n  eventBus,\n});\nawait loader.reload();\n\neventBus.on(\"my-extension:status\", (data) => console.log(data));\n```\n\n> Ver [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) y [docs/extensions.md](extensions.md)\n\n### Skills\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type Skill,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customSkill: Skill = {\n  name: \"my-skill\",\n  description: \"Custom instructions\",\n  filePath: \"/path/to/SKILL.md\",\n  baseDir: \"/path/to\",\n  source: \"custom\",\n};\n\nconst loader = new DefaultResourceLoader({\n  skillsOverride: (current) => ({\n    skills: [...current.skills, customSkill],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> Ver [examples/sdk/04-skills.ts](../examples/sdk/04-skills.ts)\n\n### Archivos de contexto\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  agentsFilesOverride: (current) => ({\n    agentsFiles: [\n      ...current.agentsFiles,\n      { path: \"/virtual/AGENTS.md\", content: \"# Guidelines\\n\\n- Be concise\" },\n    ],\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> Ver [examples/sdk/07-context-files.ts](../examples/sdk/07-context-files.ts)\n\n### Comandos de barra diagonal\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type PromptTemplate,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customCommand: PromptTemplate = {\n  name: \"deploy\",\n  description: \"Deploy the application\",\n  source: \"(custom)\",\n  content: \"# Deploy\\n\\n1. Build\\n2. Test\\n3. Deploy\",\n};\n\nconst loader = new DefaultResourceLoader({\n  promptsOverride: (current) => ({\n    prompts: [...current.prompts, customCommand],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> Ver [examples/sdk/08-prompt-templates.ts](../examples/sdk/08-prompt-templates.ts)\n\n### Gestión de sesiones\n\nLas sesiones utilizan una estructura de árbol con enlaces `id`/`parentId`, lo que permite la ramificación in situ.\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSession,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\n// In-memory (no persistence)\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n});\n\n// New persistent session\nconst { session: persisted } = await createAgentSession({\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Continue most recent\nconst { session: continued, modelFallbackMessage } = await createAgentSession({\n  sessionManager: SessionManager.continueRecent(process.cwd()),\n});\nif (modelFallbackMessage) {\n  console.log(\"Note:\", modelFallbackMessage);\n}\n\n// Open specific file\nconst { session: opened } = await createAgentSession({\n  sessionManager: SessionManager.open(\"/path/to/session.jsonl\"),\n});\n\n// List sessions\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Session replacement API for /new, /resume, /fork, /clone, and import flows.\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Replace the active session with a fresh one\nawait runtime.newSession();\n\n// Replace the active session with another saved session\nawait runtime.switchSession(\"/path/to/session.jsonl\");\n\n// Replace the active session with a fork from a specific user entry\nawait runtime.fork(\"entry-id\");\n\n// Clone the active path through a specific entry\nawait runtime.fork(\"entry-id\", { position: \"at\" });\n```\n\n**árbol de SessionManager API:**\n\n```typescript\nconst sm = SessionManager.open(\"/path/to/session.jsonl\");\n\n// Session listing\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Tree traversal\nconst entries = sm.getEntries();        // All entries (excludes header)\nconst tree = sm.getTree();              // Full tree structure\nconst path = sm.getPath();              // Path from root to current leaf\nconst leaf = sm.getLeafEntry();         // Current leaf entry\nconst entry = sm.getEntry(id);          // Get entry by ID\nconst children = sm.getChildren(id);    // Direct children of entry\n\n// Labels\nconst label = sm.getLabel(id);          // Get label for entry\nsm.appendLabelChange(id, \"checkpoint\"); // Set label\n\n// Branching\nsm.branch(entryId);                     // Move leaf to earlier entry\nsm.branchWithSummary(id, \"Summary...\");  // Branch with context summary\nsm.createBranchedSession(leafId);       // Extract path to new file\n```\n\n> Ver [examples/sdk/11-sessions.ts](../examples/sdk/11-sessions.ts) y [Session Format](session-format.md)\n\n### Gestión de configuración\n\n```typescript\nimport { createAgentSession, SettingsManager, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Default: loads from files (global + project merged)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(),\n});\n\n// With overrides\nconst settingsManager = SettingsManager.create();\nsettingsManager.applyOverrides({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 5 },\n});\nconst { session } = await createAgentSession({ settingsManager });\n\n// In-memory (no file I/O, for testing)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),\n  sessionManager: SessionManager.inMemory(),\n});\n\n// Custom directories\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(\"/custom/cwd\", \"/custom/agent\"),\n});\n```\n\n**Fábricas estáticas:**\n- `SettingsManager.create(cwd?, agentDir?)` - Cargar desde archivos\n- `SettingsManager.inMemory(settings?)` - Sin E/S de archivos\n\n**Configuraciones específicas del proyecto:**\n\nLas configuraciones se cargan desde dos ubicaciones y se fusionan:\n1. Global: `~/.pi/agent/settings.json`\n2. Proyecto: `<cwd>/.pi/settings.json`\n\nEl proyecto anula lo global. Los objetos anidados fusionan claves. Los configuradores modifican la configuración global de forma predeterminada.\n\n**Semántica de persistencia y manejo de errores:**\n\n- Los captadores/definidores de configuración son sincrónicos para el estado en memoria.\n- Los configuradores ponen en cola las escrituras persistentes de forma asincrónica.\n- Llame a `await settingsManager.flush()` cuando necesite un límite de durabilidad (por ejemplo, antes de salir del proceso o antes de afirmar el contenido del archivo en las pruebas).\n- `SettingsManager` no imprime los errores de E/S de configuración. Utilice `settingsManager.drainErrors()` e infórmelo en su capa de aplicación.\n\n> Ver [examples/sdk/10-settings.ts](../examples/sdk/10-settings.ts)\n\n## Cargador de recursos\n\nUtilice `DefaultResourceLoader` para descubrir extensiones, habilidades, indicaciones, temas y context files.\n\n```typescript\nimport {\n  DefaultResourceLoader,\n  getAgentDir,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  cwd,\n  agentDir: getAgentDir(),\n});\nawait loader.reload();\n\nconst extensions = loader.getExtensions();\nconst skills = loader.getSkills();\nconst prompts = loader.getPrompts();\nconst themes = loader.getThemes();\nconst contextFiles = loader.getAgentsFiles().agentsFiles;\n```\n\n## Valor de retorno\n\n`createAgentSession()` devuelve:\n\n```typescript\ninterface CreateAgentSessionResult {\n  // The session\n  session: AgentSession;\n  \n  // Extensions result (for runner setup)\n  extensionsResult: LoadExtensionsResult;\n  \n  // Warning if session model couldn't be restored\n  modelFallbackMessage?: string;\n}\n\ninterface LoadExtensionsResult {\n  extensions: Extension[];\n  errors: Array<{ path: string; error: string }>;\n  runtime: ExtensionRuntime;\n}\n```\n\n## Ejemplo completo\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { Type } from \"typebox\";\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  defineTool,\n  ModelRuntime,\n  SessionManager,\n  SettingsManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create({\n  authPath: \"/custom/agent/auth.json\",\n  modelsPath: \"/custom/agent/models.json\",\n});\nif (process.env.MY_KEY) {\n  await modelRuntime.setRuntimeApiKey(\"anthropic\", process.env.MY_KEY);\n}\n\n// Inline tool\nconst statusTool = defineTool({\n  name: \"status\",\n  label: \"Status\",\n  description: \"Get system status\",\n  parameters: Type.Object({}),\n  execute: async () => ({\n    content: [{ type: \"text\", text: `Uptime: ${process.uptime()}s` }],\n    details: {},\n  }),\n});\n\nconst model = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!model) throw new Error(\"Model not found\");\n\n// In-memory settings with overrides\nconst settingsManager = SettingsManager.inMemory({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 2 },\n});\n\nconst loader = new DefaultResourceLoader({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n  settingsManager,\n  systemPromptOverride: () => \"You are a minimal assistant. Be concise.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n\n  model,\n  thinkingLevel: \"off\",\n  modelRuntime,\n\n  tools: [\"read\", \"bash\", \"status\"],\n  customTools: [statusTool],\n  resourceLoader: loader,\n\n  sessionManager: SessionManager.inMemory(),\n  settingsManager,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"Get status and list files.\");\n```\n\n## Modos de ejecución\n\nLas utilidades del modo de ejecución de exportaciones SDK para crear interfaces personalizadas además de `createAgentSession()`:\n\n### Modo interactivo\n\nModo interactivo completo TUI con editor, historial de chat y todos los comandos integrados:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  InteractiveMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nconst mode = new InteractiveMode(runtime, {\n  migratedProviders: [],\n  modelFallbackMessage: undefined,\n  initialMessage: \"Hello\",\n  initialImages: [],\n  initialMessages: [],\n});\n\nawait mode.run();\n```\n\n### ejecutarModoImpresión\n\nModo de disparo único: enviar mensajes, generar resultados, salir:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runPrintMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runPrintMode(runtime, {\n  mode: \"text\",\n  initialMessage: \"Hello\",\n  initialImages: [],\n  messages: [\"Follow up\"],\n});\n```\n\n### ejecutarRpcMode\n\nModo JSON-RPC para integración de subprocesos:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runRpcMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runRpcMode(runtime);\n```\n\nConsulte [RPC documentation](rpc.md) para conocer el protocolo JSON.\n\n## RPC Modo alternativo\n\nPara la integración basada en subprocesos sin compilar con SDK, use CLI directamente:\n\n```bash\npi --mode rpc --no-session\n```\n\nConsulte [RPC documentation](rpc.md) para conocer el protocolo JSON.\n\nSe prefiere el SDK cuando:\n- Quieres seguridad tipográfica\n- Estás en el mismo proceso Node.js\n- Necesita acceso directo al estado del agente\n- Quiere personalizar herramientas/extensiones mediante programación\n\nSe prefiere el modo RPC cuando:\n- Te estás integrando desde otro idioma\n- Quieres aislamiento del proceso\n- Estás creando un cliente independiente del idioma\n\n## Exportaciones\n\nEl principal punto de entrada exporta:\n\n```typescript\n// Factory\ncreateAgentSession\ncreateAgentSessionRuntime\nAgentSessionRuntime\n\n// Auth and Models\nModelRuntime // implements pi-ai Models and owns credential storage\nModelRegistry // synchronous extension compatibility facade\nCredentialSynchronizationError\nresolveCliModel\nresolveModelScopeWithDiagnostics\n\n// Resource loading\nDefaultResourceLoader\ntype ResourceLoader\ncreateEventBus\n\n// Constants and helpers\nCONFIG_DIR_NAME\ndefineTool\ngetAgentDir\ngetPackageDir\ngetReadmePath\ngetDocsPath\ngetExamplesPath\n\n// Session management\nSessionManager\nSettingsManager\n\n// Tool factories\ncreateCodingTools\ncreateReadOnlyTools\ncreateReadTool, createBashTool, createEditTool, createWriteTool\ncreateGrepTool, createFindTool, createLsTool\n\n// Types\ntype CreateAgentSessionOptions\ntype CreateAgentSessionResult\ntype ExtensionFactory\ntype InlineExtension\ntype ExtensionAPI\ntype ToolDefinition\ntype Skill\ntype PromptTemplate\ntype Tool\n```\n\nPara conocer los tipos de extensión, consulte [extensions.md](extensions.md) para obtener el API completo.","sourceFile":"sdk.md"},"security":{"title":"Seguridad","markdown":"Pi es un agente de codificación local. Se ejecuta con los permisos de la cuenta de usuario que lo inicia y trata los archivos en los que ese usuario puede escribir como si estuvieran dentro del mismo límite de confianza local.\n\n## Confianza del proyecto\n\nLa confianza del proyecto controla si pi carga configuraciones, recursos, paquetes y extensiones locales del proyecto. No es un sandbox y no restringe lo que el modelo puede pedirle a las herramientas que hagan después de comenzar a trabajar en un directorio.\n\nPi considera que un proyecto tiene recursos que requieren confianza cuando encuentra alguno de estos en el directorio de trabajo actual:\n\n- `.pi/settings.json`\n- `.pi/extensions`, `.pi/skills`, `.pi/prompts` o `.pi/themes`\n- `.pi/SYSTEM.md` o `.pi/APPEND_SYSTEM.md`\n- proyecto `.agents/skills` en el directorio actual o en un directorio ancestral\n\nUn directorio `.pi` simple no cuenta como un recurso de proyecto que requiera confianza.\n\nCuando se inicia una sesión interactiva en un proyecto con recursos que requieren confianza y sin una decisión guardada para el directorio actual o un directorio principal, pi sigue a `defaultProjectTrust` desde la configuración global. El valor predeterminado es `\"ask\"`, que pregunta si se debe confiar en el proyecto cuando la interfaz de usuario está disponible. Las decisiones guardadas se almacenan en el directorio canónico en `~/.pi/agent/trust.json`, y la decisión guardada más cercana en la ruta actual o principal se aplica antes del valor predeterminado global.\n\nConfiar en un proyecto le permite a pi cargar recursos del proyecto que requieren confianza, incluidos:\n\n- `.pi/settings.json`\n- `.pi` recursos como extensiones, habilidades, prompt templates, temas y archivos de avisos del sistema\n- Faltan paquetes de proyectos configurados a través de la configuración del proyecto.\n- extensiones locales del proyecto y extensiones administradas por paquetes del proyecto\n\nLa disminución de la confianza omite los recursos protegidos. Los archivos de contexto como `AGENTS.override.md`, `AGENTS.md` y `CLAUDE.md` se cargan independientemente de la confianza del proyecto, a menos que la carga de contexto esté deshabilitada. Antes de que se resuelva la confianza, pi solo carga context files, extensiones de usuario/globales y extensiones CLI `-e`. Las extensiones usuario/global y CLI pueden manejar el evento `project_trust`; la primera extensión que devuelve una decisión de sí/no es propietaria de la decisión.\n\nLos modos no interactivos (`-p`, `--mode json` y `--mode rpc`) no muestran un mensaje de confianza. Sin una decisión de confianza guardada aplicable, `defaultProjectTrust: \"ask\"` y `\"never\"` ignoran dichos recursos, mientras que `\"always\"` confía en ellos. Utilice `--approve`/`-a` o `--no-approve`/`-na` para anular la confianza del proyecto durante una ejecución.\n\n## Sin zona de pruebas incorporada\n\nPi no incluye un sandbox incorporado. Las herramientas integradas pueden leer archivos, escribir archivos, editar archivos y ejecutar comandos de shell con los permisos del proceso pi. Extensions son módulos TypeScript que se ejecutan con los mismos permisos. Las instalaciones de paquetes, los comandos de shell, los servidores de idiomas, los comandos de prueba y otras herramientas de desarrollo se comportan como procesos locales normales.\n\nEsto es intencional. Pi está diseñado para operar en árboles de fuentes locales, invocar cadenas de herramientas del proyecto e integrarse con el entorno de desarrollo existente del usuario. Un proceso parcial sandbox sería fácil de malinterpretar como un límite de seguridad y al mismo tiempo dependería del shell del host, el sistema de archivos, los administradores de paquetes, las credenciales y el código de extensión. El aislamiento real debe provenir del sistema operativo o de un límite de virtualización/contenedor.\n\nLa confianza en el proyecto es sólo una protección de carga de entradas. Evita que un repositorio cambie silenciosamente la configuración o las extensiones de pi antes de que usted lo apruebe. No hace que el código que no es de confianza, las indicaciones que no son de confianza o la salida de un modelo que no es de confianza sean seguros. Se espera que la inyección rápida desde archivos del repositorio, comentarios, documentación, context files o resultados de compilación sea un riesgo para el agente local y pi no puede prevenirlo de manera confiable.\n\n## Ejecutar trabajo que no es de confianza o no supervisado\n\nPara repositorios que no son de confianza, código generado que no desea monitorear de cerca o automatización desatendida, ejecute pi en un entorno contenido. Utilice un contenedor, VM, micro-VM, remoto sandbox o controlado por políticas sandbox con solo los archivos y credenciales necesarios para la tarea.\n\nLos patrones comunes están documentados en [Containerization](containerization.md):\n\n- ejecutar todo el proceso `pi` dentro de un contenedor/sandbox\n- ejecute host pi mientras enruta la ejecución de la herramienta incorporada a una micro-VM Gondolin\n- montar solo las rutas del espacio de trabajo a las que debe acceder el agente\n- evite montar el host `~/.pi/agent` a menos que el contenedor deba acceder a las sesiones, configuraciones y credenciales del host\n- pasar el mínimo requerido API keys o usar credenciales de corta duración\n- restringir el acceso a la red cuando la tarea no lo necesita\n- revise las diferencias y los resultados antes de copiar los resultados a sistemas confiables\n\nSi realiza un montaje vinculante de lectura/escritura en un espacio de trabajo del host, las escrituras desde el interior del contenedor o la máquina virtual aún pueden modificar los archivos del host. Utilice montajes de solo lectura o copie archivos dentro y fuera del sandbox cuando necesite una protección más sólida contra escrituras no deseadas.\n\n## Informar problemas de seguridad\n\nPara informar un problema de seguridad, siga el repositorio [Security Policy](https://github.com/earendil-works/pi-mono/blob/main/SECURITY.md). No abra una edición pública para informes sensibles a la seguridad.\n\nEl comportamiento esperado del agente local, la falta de un sandbox integrado, la inyección rápida de contenido no confiable y el comportamiento de las extensiones o habilidades instaladas por el usuario generalmente están fuera de los límites de seguridad, a menos que el informe demuestre una verdadera omisión de los límites de privilegios o muestre cómo pi otorga acceso que el usuario local aún no tenía.","sourceFile":"security.md"},"session-format":{"title":"Formato de archivo de sesión","markdown":"Las sesiones se almacenan como archivos JSONL (JSON Líneas). Cada línea es un objeto JSON con un campo `type`. Las entradas de sesión forman una estructura de árbol a través de los campos `id`/`parentId`, lo que permite la bifurcación in situ sin crear nuevos archivos.\n\n## Ubicación del archivo\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl\n```\n\nDonde `<path>` es el directorio de trabajo con `/` reemplazado por `-`.\n\n## Eliminar sesiones\n\nLas sesiones se pueden eliminar eliminando sus archivos `.jsonl` en `~/.pi/agent/sessions/`.\n\nPi también admite la eliminación de sesiones de forma interactiva desde `/resume` (seleccione una sesión y presione `Ctrl+D`, luego confirme). Cuando está disponible, pi usa `trash` CLI para evitar la eliminación permanente.\n\n## Versión de sesión\n\nLas sesiones tienen un campo de versión en el encabezado:\n\n- **Versión 1**: Secuencia de entrada lineal (heredada, migrada automáticamente al cargar)\n- **Versión 2**: Estructura de árbol con enlaces `id`/`parentId`\n- **Versión 3**: Se cambió el nombre del rol `hookMessage` a `custom` (unificación de extensiones)\n\nLas sesiones existentes se migran automáticamente a la versión actual (v3) cuando se cargan.\n\n## Archivos fuente\n\nFuente en GitHub ([pi-mono](https://github.com/earendil-works/pi-mono)):\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - Tipos de entrada de sesión y SessionManager\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts) - Tipos de mensajes extendidos (BashExecutionMessage, CustomMessage, etc.)\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts) - Tipos de mensajes base (UserMessage, AssistantMessage, ToolResultMessage)\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts) - Tipo de unión AgentMessage\n\nPara las definiciones de TypeScript en su proyecto, inspeccione `node_modules/@earendil-works/pi-coding-agent/dist/` y `node_modules/@earendil-works/pi-ai/dist/`.\n\n## Tipos de mensajes\n\nLas entradas de sesión contienen `AgentMessage` objetos. Comprender estos tipos es esencial para analizar sesiones y escribir extensiones.\n\n### Bloques de contenido\n\nLos mensajes contienen matrices de bloques de contenido escritos:\n\n```typescript\ninterface TextContent {\n  type: \"text\";\n  text: string;\n}\n\ninterface ImageContent {\n  type: \"image\";\n  data: string;      // base64 encoded\n  mimeType: string;  // e.g., \"image/jpeg\", \"image/png\"\n}\n\ninterface ThinkingContent {\n  type: \"thinking\";\n  thinking: string;\n}\n\ninterface ToolCall {\n  type: \"toolCall\";\n  id: string;\n  name: string;\n  arguments: Record<string, any>;\n}\n```\n\n### Tipos de mensajes básicos (de pi-ai)\n\n```typescript\ninterface UserMessage {\n  role: \"user\";\n  content: string | (TextContent | ImageContent)[];\n  timestamp: number;  // Unix ms\n}\n\ninterface AssistantMessage {\n  role: \"assistant\";\n  content: (TextContent | ThinkingContent | ToolCall)[];\n  api: string;\n  provider: string;\n  model: string;\n  usage: Usage;\n  stopReason: \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n  timestamp: number;\n}\n\ninterface ToolResultMessage {\n  role: \"toolResult\";\n  toolCallId: string;\n  toolName: string;\n  content: (TextContent | ImageContent)[];\n  details?: any;      // Tool-specific metadata\n  usage?: Usage;      // Nested LLM work performed by the tool\n  isError: boolean;\n  timestamp: number;\n}\n\ninterface Usage {\n  input: number;\n  output: number;\n  cacheRead: number;\n  cacheWrite: number;\n  totalTokens: number;\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n    total: number;\n  };\n}\n```\n\nEl tipo pi-ai exportado `StopReason` también incluye `\"pending\"`, pero ese valor está reservado para mensajes parciales en eventos de transmisión. Los mensajes del terminal `done`/`error` lo reemplazan con un motivo de finalización antes de que pi persista en el mensaje del asistente, por lo que `\"pending\"` nunca debería aparecer en la sesión JSONL.\n\n### Tipos de mensajes extendidos (de pi-coding-agent)\n\n```typescript\ninterface BashExecutionMessage {\n  role: \"bashExecution\";\n  command: string;\n  output: string;\n  exitCode: number | undefined;\n  cancelled: boolean;\n  truncated: boolean;\n  fullOutputPath?: string;\n  excludeFromContext?: boolean;  // true for !! prefix commands\n  timestamp: number;\n}\n\ninterface CustomMessage {\n  role: \"custom\";\n  customType: string;            // Extension identifier\n  content: string | (TextContent | ImageContent)[];\n  display: boolean;              // Show in TUI\n  details?: any;                 // Extension-specific metadata\n  timestamp: number;\n}\n\ninterface BranchSummaryMessage {\n  role: \"branchSummary\";\n  summary: string;\n  fromId: string;                // Entry we branched from\n  timestamp: number;\n}\n\ninterface CompactionSummaryMessage {\n  role: \"compactionSummary\";\n  summary: string;\n  tokensBefore: number;\n  timestamp: number;\n}\n```\n\n### Unión de mensajes de agente\n\n```typescript\ntype AgentMessage =\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\n## Base de entrada\n\nTodas las entradas (excepto `SessionHeader`) extienden `SessionEntryBase`:\n\n```typescript\ninterface SessionEntryBase {\n  type: string;\n  id: string;           // 8-char hex ID\n  parentId: string | null;  // Parent entry ID (null for first entry)\n  timestamp: string;    // ISO timestamp\n}\n```\n\n## Tipos de entrada\n\n### Encabezado de sesión\n\nPrimera línea del archivo. Solo metadatos, no parte del árbol (no `id`/`parentId`).\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\"}\n```\n\nPara sesiones con uno de los padres (creadas mediante `/fork`, `/clone` o `newSession({ parentSession })`):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\",\"parentSession\":\"/path/to/original/session.jsonl\"}\n```\n\n### Entrada de mensaje de sesión\n\nUn mensaje en la conversación. El campo `message` contiene un `AgentMessage`.\n\n```json\n{\"type\":\"message\",\"id\":\"a1b2c3d4\",\"parentId\":\"prev1234\",\"timestamp\":\"2024-12-03T14:00:01.000Z\",\"message\":{\"role\":\"user\",\"content\":\"Hello\"}}\n{\"type\":\"message\",\"id\":\"b2c3d4e5\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:00:02.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Hi!\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}}\n{\"type\":\"message\",\"id\":\"c3d4e5f6\",\"parentId\":\"b2c3d4e5\",\"timestamp\":\"2024-12-03T14:00:03.000Z\",\"message\":{\"role\":\"toolResult\",\"toolCallId\":\"call_123\",\"toolName\":\"bash\",\"content\":[{\"type\":\"text\",\"text\":\"output\"}],\"isError\":false}}\n```\n\n### Entrada de cambio de modelo\n\nEmitido cuando el usuario cambia de modelo a mitad de sesión.\n\n```json\n{\"type\":\"model_change\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:05:00.000Z\",\"provider\":\"openai\",\"modelId\":\"gpt-4o\"}\n```\n\n### PensamientoNivelCambioEntrada\n\nEmitido cuando el usuario cambia el nivel de pensamiento/razonamiento.\n\n```json\n{\"type\":\"thinking_level_change\",\"id\":\"e5f6g7h8\",\"parentId\":\"d4e5f6g7\",\"timestamp\":\"2024-12-03T14:06:00.000Z\",\"thinkingLevel\":\"high\"}\n```\n\n### Entrada de compactación\n\nCreado cuando se compacta el contexto. Almacena un resumen de mensajes anteriores.\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"firstKeptEntryId\":\"c3d4e5f6\",\"tokensBefore\":50000}\n```\n\nLas compactaciones más nuevas generadas por arnés incorporan el contexto posterior a la compactación retenido directamente en la entrada, en lugar de `firstKeptEntryId`:\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"tokensBefore\":50000,\"retainedTail\":[{\"role\":\"user\",\"content\":\"latest request\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"latest reply\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}]}\n```\n\nCampos opcionales:\n- `usage`: uso de LLM desde la generación del resumen; incluido en el token de sesión y los costos totales\n- `retainedTail`: Materializado `AgentMessage[]` mantenido después de la compactación. Esto es opcional sólo por compatibilidad con sesiones anteriores. Las compactaciones más nuevas generadas por arnés lo incluyen para que podamos reconstruir el contexto desde este punto de control sin tener que recorrer las entradas más antiguas antes de la entrada de compactación.\n- `details`: datos específicos de la implementación (por ejemplo, `{ readFiles: string[], modifiedFiles: string[] }` para datos predeterminados o personalizados para extensiones)\n- `fromHook`: `true` si es generado por una extensión, `false`/`undefined` si es generado por pi (nombre de campo heredado)\n- `firstKeptEntryId`: por compatibilidad con el formato de entrada anterior.\n\n### RamaResumenEntrada\n\nCreado al cambiar de rama a través de `/tree` con un resumen generado por LLM de la rama izquierda hasta el ancestro común. Captura el contexto del camino abandonado.\n\n```json\n{\"type\":\"branch_summary\",\"id\":\"g7h8i9j0\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:15:00.000Z\",\"fromId\":\"f6g7h8i9\",\"summary\":\"Branch explored approach A...\"}\n```\n\nCampos opcionales:\n- `usage`: uso de LLM desde la generación del resumen; incluido en el token de sesión y los costos totales\n- `details`: datos de seguimiento de archivos (`{ readFiles: string[], modifiedFiles: string[] }`) para datos predeterminados o personalizados para extensiones\n- `fromHook`: `true` si es generado por una extensión, `false`/`undefined` si es generado por pi (nombre de campo heredado)\n\n### Entrada personalizada\n\nPersistencia del estado de extensión. NO participa en el contexto LLM.\n\n```json\n{\"type\":\"custom\",\"id\":\"h8i9j0k1\",\"parentId\":\"g7h8i9j0\",\"timestamp\":\"2024-12-03T14:20:00.000Z\",\"customType\":\"my-extension\",\"data\":{\"count\":42}}\n```\n\nUtilice `customType` para identificar las entradas de su extensión al recargar. El modo interactivo puede representar entradas personalizadas a través de `pi.registerEntryRenderer(customType, renderer)`, pero aún no participan en el contexto LLM.\n\n### Entrada de mensaje personalizado\n\nMensajes inyectados con extensión que SÍ participan en el contexto LLM.\n\n```json\n{\"type\":\"custom_message\",\"id\":\"i9j0k1l2\",\"parentId\":\"h8i9j0k1\",\"timestamp\":\"2024-12-03T14:25:00.000Z\",\"customType\":\"my-extension\",\"content\":\"Injected context...\",\"display\":true}\n```\n\nCampos:\n- `content`: Cadena o `(TextContent | ImageContent)[]` (igual que UserMessage)\n- `display`: `true` = mostrar en TUI con un estilo distinto, `false` = oculto\n- `details`: metadatos específicos de la extensión opcionales (no enviados a LLM)\n\n### Entrada de etiqueta\n\nMarcador/marcador definido por el usuario en una entrada.\n\n```json\n{\"type\":\"label\",\"id\":\"j0k1l2m3\",\"parentId\":\"i9j0k1l2\",\"timestamp\":\"2024-12-03T14:30:00.000Z\",\"targetId\":\"a1b2c3d4\",\"label\":\"checkpoint-1\"}\n```\n\nEstablezca `label` en `undefined` para borrar una etiqueta.\n\n### Entrada de información de sesión\n\nMetadatos de la sesión (por ejemplo, nombre para mostrar definido por el usuario). Establecer mediante `/name`, `--name` / `-n` o `pi.setSessionName()` en extensiones.\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\nEl nombre de la sesión se muestra en el selector de sesión (`/resume`) en lugar del primer mensaje cuando se configura.\n\n## Estructura de árbol\n\nLas entradas forman un árbol:\n- La primera entrada tiene `parentId: null`\n- Cada entrada posterior apunta a su padre mediante `parentId`\n- La ramificación crea nuevos hijos a partir de una entrada anterior.\n- La \"hoja\" es la posición actual en el árbol.\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## Construcción de contexto\n\n`buildContextEntries()` camina desde la hoja actual hasta la raíz, generando la lista de entradas activas respetando la compactación:\n\n1. Recoge todas las entradas en el camino.\n2. Si hay un `CompactionEntry` en el camino:\n   - Incluye la entrada de compactación primero.\n   - Si `retainedTail` está presente, actúa como un punto de control autónomo y se incluyen las entradas después de la compactación.\n   - De lo contrario se incluyen las entradas desde `firstKeptEntryId` a la compactación.\n   - Luego se incluyen las entradas después de la compactación.\n3. Conserva las entradas que no son mensajes en el rango seleccionado para que el modo interactivo pueda representarlas\n\n`buildSessionContext()` se basa en esa lista de entradas para producir la lista de mensajes para el LLM:\n\n1. Extrae el modelo actual y la configuración del nivel de pensamiento de la ruta completa.\n2. Convierte entradas seleccionadas en mensajes:\n   - `message` -> almacenado `AgentMessage`\n   - `compaction` -> `compactionSummary` más `retainedTail` cuando esté presente\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `custom` -> sin mensaje de contexto\n\nEsto hace que las compactaciones más nuevas actúen como puntos de control autónomos. `retainedTail` es opcional solo para que las sesiones más antiguas que solo almacenan `firstKeptEntryId` continúen cargándose correctamente.\n\n## Ejemplo de análisis\n\n```typescript\nimport { readFileSync } from \"fs\";\n\nconst lines = readFileSync(\"session.jsonl\", \"utf8\").trim().split(\"\\n\");\n\nfor (const line of lines) {\n  const entry = JSON.parse(line);\n\n  switch (entry.type) {\n    case \"session\":\n      console.log(`Session v${entry.version ?? 1}: ${entry.id}`);\n      break;\n    case \"message\":\n      console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);\n      break;\n    case \"compaction\":\n      console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);\n      break;\n    case \"branch_summary\":\n      console.log(`[${entry.id}] Branch from ${entry.fromId}`);\n      break;\n    case \"custom\":\n      console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);\n      break;\n    case \"custom_message\":\n      console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);\n      break;\n    case \"label\":\n      console.log(`[${entry.id}] Label \"${entry.label}\" on ${entry.targetId}`);\n      break;\n    case \"model_change\":\n      console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);\n      break;\n    case \"thinking_level_change\":\n      console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);\n      break;\n  }\n}\n```\n\n## Administrador de sesión API\n\nMétodos clave para trabajar con sesiones mediante programación.\n\n### Métodos de creación estática\n- `SessionManager.create(cwd, sessionDir?)` - Nueva sesión\n- `SessionManager.open(path, sessionDir?)` - Abrir archivo de sesión existente\n- `SessionManager.continueRecent(cwd, sessionDir?)` - Continuar con el más reciente o crear uno nuevo\n- `SessionManager.inMemory(cwd?)` - Sin persistencia de archivos\n- `SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)` - Sesión bifurcada de otro proyecto\n\n### Métodos de listado estático\n- `SessionManager.list(cwd, sessionDir?, onProgress?)` - Listar sesiones para un directorio\n- `SessionManager.listAll(onProgress?)`: enumera todas las sesiones de todos los proyectos\n\n### Métodos de instancia: gestión de sesiones\n- `newSession(options?)` - Iniciar una nueva sesión (opciones: `{ parentSession?: string }`)\n- `setSessionFile(path)` - Cambiar a un archivo de sesión diferente\n- `createBranchedSession(leafId)` - Extraer rama a un nuevo archivo de sesión\n\n### Métodos de instancia: anexar (todos los ID de entrada devueltos)\n- `appendMessage(message)` - Agregar mensaje\n- `appendThinkingLevelChange(level)` - Registrar cambio de pensamiento\n- `appendModelChange(provider, modelId)` - Cambio de modelo de registro\n- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Agregar compactación\n- `appendCustomEntry(customType, data?)` - Estado de extensión (no en contexto)\n- `appendSessionInfo(name)` - Establecer nombre para mostrar de la sesión\n- `appendCustomMessageEntry(customType, content, display, details?)` - Mensaje de extensión (en contexto)\n- `appendLabelChange(targetId, label)` - Establecer/borrar etiqueta\n\n### Métodos de instancia: navegación en árbol\n- `getLeafId()` - Posición actual\n- `getLeafEntry()` - Obtener la entrada de la hoja actual\n- `getEntry(id)` - Obtener entrada por ID\n- `getBranch(fromId?)` - Camina desde la entrada hasta la raíz\n- `getTree()` - Obtener estructura de árbol completa\n- `getChildren(parentId)` - Obtener hijos directos\n- `getLabel(id)` - Obtener etiqueta para ingresar\n- `branch(entryId)` - Mover hoja a la entrada anterior\n- `resetLeaf()` - Restablecer la hoja a nula (antes de cualquier entrada)\n- `branchWithSummary(entryId, summary, details?, fromHook?)` - Rama con resumen de contexto\n\n### Métodos de instancia: contexto e información\n- `buildContextEntries()` - Obtener entradas de sucursales activas con compactación aplicada\n- `buildSessionContext()` - Recibe mensajes, nivel de pensamiento y modelo para LLM\n- `getEntries()` - Todas las entradas (excluyendo el encabezado)\n- `getHeader()` - Metadatos del encabezado de sesión\n- `getSessionName()`: obtiene el nombre para mostrar de la última entrada de session_info\n- `getCwd()` - Directorio de trabajo\n- `getSessionDir()` - Directorio de almacenamiento de sesiones\n- `getSessionId()` - UUID de sesión\n- `getSessionFile()` - Ruta del archivo de sesión (no definida para en memoria)\n- `isPersisted()`: si la sesión se guarda en el disco","sourceFile":"session-format.md"},"sessions":{"title":"Sesiones","markdown":"Pi guarda conversaciones como sesiones para que puedas continuar trabajando, avanzar desde turnos anteriores y volver a visitar rutas anteriores.\n\n## Almacenamiento de sesiones\n\nLas sesiones se guardan automáticamente en `~/.pi/agent/sessions/`, organizadas por directorio de trabajo. Cada sesión es un archivo JSONL con una estructura de árbol.\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select from past sessions\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or partial session ID\npi --fork <path|id>    # Fork a session file or partial session ID into a new session\n```\n\nUtilice `/session` en modo interactivo para ver el archivo de sesión actual, el ID de sesión, el recuento de mensajes, los tokens y el costo.\n\nPara el formato de archivo JSONL y SessionManager API, consulte [Session Format](session-format.md).\n\n## Comandos de sesión\n\n| Dominio | Descripción |\n|---------|-------------|\n| `/resume` | Explorar y seleccionar sesiones anteriores |\n| `/new` | Iniciar una nueva sesión |\n| `/name <name>` | Establecer el nombre para mostrar de la sesión actual |\n| `/session` | Mostrar información de la sesión |\n| `/tree` | Navegar por el session tree actual |\n| `/fork` | Crear una nueva sesión a partir de un mensaje de usuario anterior |\n| `/clone` | Duplicar la rama activa actual en una nueva sesión |\n| `/compact [prompt]` | Resumir el contexto anterior; ver [Compaction](compaction.md) |\n| `/export [file]` | Exportar sesión a HTML |\n| `/share` | Subir como GitHub privado con enlace HTML para compartir |\n\n## Reanudar y eliminar sesiones\n\n`/resume` abre un selector de sesión interactivo para el proyecto actual. `pi -r` abre el mismo selector al inicio.\n\nEn el selector puedes:\n\n- buscar escribiendo\n- alternar visualización de ruta con Ctrl+P\n- alternar el modo de clasificación con Ctrl+S\n- filtrar a sesiones con nombre con Ctrl+N\n- cambiar el nombre con Ctrl+R\n- eliminar con Ctrl+D, luego confirmar\n\nCuando está disponible, pi usa `trash` CLI para eliminar en lugar de eliminar archivos permanentemente.\n\n## Sesiones de nombres\n\nUtilice `/name <name>` para establecer un nombre de sesión legible por humanos:\n\n```text\n/name Refactor auth module\n```\n\nEstablezca el nombre al inicio con `--name` o `-n`:\n\n```bash\npi --name \"Refactor auth module\"\npi --name \"CI audit\" -p \"Review this build failure\"\n```\n\nLas sesiones con nombre son más fáciles de encontrar en `/resume` y `pi -r`.\n\n## Ramificando con `/tree`\n\nLas sesiones se almacenan como árboles. Cada entrada tiene un `id` y un `parentId`, y la posición actual es la hoja activa. `/tree` te permite saltar a cualquier punto anterior y continuar desde allí sin crear un archivo nuevo.\n\n<p align=\"center\"><img src=\"images/tree-view.png\" alt=\"Tree View\" width=\"600\"></p>\n\nForma de ejemplo:\n\n```text\n├─ user: \"Hello, can you help...\"\n│  └─ assistant: \"Of course! I can...\"\n│     ├─ user: \"Let's try approach A...\"\n│     │  └─ assistant: \"For approach A...\"\n│     │     └─ user: \"That worked...\"  ← active\n│     └─ user: \"Actually, approach B...\"\n│        └─ assistant: \"For approach B...\"\n```\n\n### Controles de árbol\n\n| Llave | Acción |\n|-----|--------|\n| ↑/↓ | Navegar por entradas visibles |\n| ←/→ | Página arriba/abajo |\n| Ctrl+←/Ctrl+→ o Alt+←/Alt+→ | Doblar/desplegar o saltar entre segmentos de ramas |\n| Mayús+L | Establecer o borrar una etiqueta en la entrada seleccionada |\n| Mayús+T | Alternar marcas de tiempo de etiquetas |\n| Ingresar | Seleccionar entrada |\n| Esc/Ctrl+C | Cancelar |\n| Ctrl+O | Modo de filtro de ciclo |\n\nLos modos de filtro son: predeterminado, sin herramientas, solo para usuario, solo con etiquetas y todos. Configure el valor predeterminado con `treeFilterMode` en [Settings](settings.md).\n\n### Comportamiento de selección\n\nSeleccionar un usuario o mensaje personalizado:\n\n1. Mueve la hoja al padre del mensaje seleccionado.\n2. Coloca el texto del mensaje seleccionado en el editor.\n3. Le permite editar y volver a enviar, creando una nueva rama.\n\nSeleccionar un asistente, herramienta, compactación u otra entrada que no sea de usuario:\n\n1. Mueve la hoja a esa entrada.\n2. Deja el editor vacío.\n3. Le permite continuar desde ese punto.\n\nAl seleccionar el mensaje del usuario raíz, la hoja se restablece a una conversación vacía y se coloca el mensaje original en el editor.\n\n## `/tree`, `/fork` y `/clone`\n\n| Característica | `/tree` | `/fork` | `/clone` |\n|---------|---------|---------|----------|\n| Producción | Mismo archivo de sesión | Nuevo archivo de sesión | Nuevo archivo de sesión |\n| Vista | árbol completo | Selector de mensajes de usuario | Rama activa actual |\n| Uso típico | Explorar alternativas existentes | Iniciar una nueva sesión desde un mensaje anterior | Duplicar el trabajo actual antes de continuar |\n| Resumen | Resumen de rama opcional | Ninguno | Ninguno |\n\nUtilice `/tree` cuando desee mantener las alternativas juntas. Utilice `/fork` o `/clone` cuando desee un archivo de sesión independiente.\n\n## Resúmenes de sucursales\n\nCuando `/tree` cambia de una rama a otra, pi puede resumir la rama abandonada y adjuntar ese resumen en la nueva posición. Esto preserva el contexto importante del camino que dejaste sin volver a reproducir toda la rama.\n\nCuando se le solicite, elija uno de:\n\n1. sin resumen\n2. resumir con el mensaje predeterminado\n3. resumir con instrucciones de enfoque personalizadas\n\nConsulte [Compaction](compaction.md) para ver branch summarization partes internas y ganchos de extensión.\n\n## Formato de sesión\n\nLos archivos de sesión son JSONL y contienen entradas de mensajes, cambios de modelo, cambios de nivel de pensamiento, etiquetas, compactaciones, resúmenes de ramas y entradas de extensión.\n\nPara analizadores, extensiones, uso de SDK y el SessionManager completo API, consulte [Session Format](session-format.md).","sourceFile":"sessions.md"},"settings":{"title":"Ajustes","markdown":"Pi utiliza archivos de configuración JSON con la configuración del proyecto anulando la configuración global.\n\n| Ubicación | Alcance |\n|----------|-------|\n| `~/.pi/agent/settings.json` | Global (todos los proyectos) |\n| `.pi/settings.json` | Proyecto (directorio actual) |\n\nEdite directamente o use `/settings` para opciones comunes.\n\n## Confianza del proyecto\n\nEn el inicio interactivo, pi pregunta antes de confiar en una carpeta de proyecto que contiene configuraciones, recursos o proyecto local del proyecto `.agents/skills` y no tiene ninguna decisión guardada para la carpeta o una carpeta principal en `~/.pi/agent/trust.json`. Confiar en un proyecto permite a pi cargar recursos `.pi/settings.json` y `.pi`, instalar paquetes de proyecto faltantes y ejecutar extensiones de proyecto.\n\nLos modos no interactivos (`-p`, `--mode json` y `--mode rpc`) no muestran un mensaje de confianza. Sin una decisión de confianza guardada aplicable, usan `defaultProjectTrust` de la configuración global: `ask` (predeterminado) y `never` ignoran esos recursos del proyecto, mientras que `always` confía en ellos. Pase `--approve`/`-a` o `--no-approve`/`-na` para anular la confianza del proyecto durante una ejecución.\n\nSi no se aplica ninguna extensión o decisión guardada, `defaultProjectTrust` controla el comportamiento de reserva. Configúrelo en `\"ask\"`, `\"always\"` o `\"never\"` en `~/.pi/agent/settings.json`, o cámbielo con `/settings`.\n\nLos comandos `pi config` y del paquete usan el mismo flujo de confianza del proyecto, excepto que `pi update` nunca lo solicita. Pase `--approve` para confiar en la configuración local del proyecto para un comando o `--no-approve` para ignorarlos.\n\nUtilice `/trust` en modo interactivo para guardar una decisión de confianza del proyecto para sesiones futuras, incluida la confianza para la carpeta principal inmediata. Escribe solo `~/.pi/agent/trust.json`; la sesión actual no se recarga, así que reinicie pi para que los cambios surtan efecto.\n\n## Todas las configuraciones\n\n### Modelo y pensamiento\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `defaultProvider` | cadena | - | Proveedor predeterminado (por ejemplo, `\"anthropic\"`, `\"openai\"`) |\n| `defaultModel` | cadena | - | ID de modelo predeterminado |\n| `defaultThinkingLevel` | cadena | - | `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"` |\n| `hideThinkingBlock` | booleano | `false` | Ocultar bloques de pensamiento en la salida |\n| `showCacheMissNotices` | booleano | `false` | Mostrar avisos de transcripción en caso de errores significativos en la caché de avisos |\n| `thinkingBudgets` | objeto | - | Presupuestos de tokens personalizados por nivel de pensamiento |\n\n#### pensandoPresupuestos\n\n```json\n{\n  \"thinkingBudgets\": {\n    \"minimal\": 1024,\n    \"low\": 4096,\n    \"medium\": 10240,\n    \"high\": 32768\n  }\n}\n```\n\n### Interfaz de usuario y pantalla\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `theme` | cadena | `\"dark\"` | Nombre del tema (`\"dark\"`, `\"light\"` o personalizado) |\n| `externalEditor` | cadena | `$VISUAL`, luego `$EDITOR`, luego Bloc de notas en Windows o `nano` en otro lugar | Comando para Ctrl+G editor externo; tiene prioridad sobre las variables de entorno |\n| `quietStartup` | booleano | `false` | Ocultar encabezado de inicio |\n| `defaultProjectTrust` | cadena | `\"ask\"` | Comportamiento de confianza del proyecto alternativo: `\"ask\"`, `\"always\"` o `\"never\"`. Sólo configuración global |\n| `collapseChangelog` | booleano | `false` | Mostrar registro de cambios condensado después de las actualizaciones |\n| `enableInstallTelemetry` | booleano | `true` | Envíe un ping anónimo de instalación/actualización de la versión después de la primera instalación o de las actualizaciones detectadas por el registro de cambios. Esto no controla las comprobaciones de actualizaciones. |\n| `enableAnalytics` | booleano | `false` | Opte por compartir datos analíticos. Actualmente solo se solicita durante la configuración experimental por primera vez (`PI_EXPERIMENTAL=1`) |\n| `trackingId` | cadena | - | Identificador de seguimiento de análisis, generado cuando `enableAnalytics` está activado |\n| `doubleEscapeAction` | cadena | `\"tree\"` | Acción para doble escape: `\"tree\"`, `\"fork\"` o `\"none\"` |\n| `treeFilterMode` | cadena | `\"default\"` | Filtro predeterminado para `/tree`: `\"default\"`, `\"no-tools\"`, `\"user-only\"`, `\"labeled-only\"`, `\"all\"` |\n| `editorPaddingX` | número | `0` | Relleno horizontal para el editor de entrada (0-3) |\n| `outputPad` | número | `1` | Relleno horizontal para mensajes de usuario, mensajes de asistente y pensamiento (0 o 1) |\n| `autocompleteMaxVisible` | número | `5` | Máximo de elementos visibles en el menú desplegable de autocompletar (3-20) |\n| `showHardwareCursor` | booleano | `false` | Muestre el cursor del terminal mientras TUI lo posiciona para soporte IME |\n| `tuiMode` | cadena | `\"regular\"` | Modo interactivo TUI: `\"regular\"` o experimental `\"fullscreen\"`. Los cambios de `/settings` se aplican inmediatamente; `--tui-mode` anula esta configuración al inicio |\n| `fullscreenExitOutput` | cadena | `\"transcript\"` | Salida de salida en pantalla completa: `\"transcript\"` imprime la transcripción final y la sugerencia de reanudación, mientras que `\"resume-hint\"` restaura la pantalla anterior e imprime solo la sugerencia de reanudación. No tiene ningún efecto en el modo normal TUI |\n| `fullscreenScrollbar` | cadena | `\"auto\"` | Barra de desplazamiento de transcripción de pantalla completa: `\"auto\"` la muestra temporalmente mientras se desplaza, `\"always\"` reserva la columna más a la derecha y la mantiene visible, y `\"hidden\"` la oculta. No tiene ningún efecto en el modo normal TUI |\n\nPara VS Code, incluya `--wait` para que pi se reanude después de que salga el editor:\n\n```json\n{\n  \"externalEditor\": \"code --wait\"\n}\n```\n\n### Comprobaciones de telemetría y actualización.\n\n`enableInstallTelemetry` solo controla el ping anónimo de instalación/actualización a `https://pi.dev/api/report-install`. La exclusión voluntaria de la telemetría no deshabilita las comprobaciones de actualizaciones; Pi aún puede recuperar `https://pi.dev/api/latest-version` para buscar la última versión.\n\nConfigure `PI_SKIP_VERSION_CHECK=1` para deshabilitar la verificación de actualización de la versión Pi. Utilice `--offline` o `PI_OFFLINE=1` para deshabilitar todas las operaciones de red de inicio que se describen aquí, incluidas las comprobaciones de actualización, las comprobaciones de actualización de paquetes y la telemetría de instalación/actualización.\n\n### Red\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `httpProxy` | cadena | - | La URL del proxy HTTP se aplica como `HTTP_PROXY` y `HTTPS_PROXY`. Sólo configuración global. |\n\n```json\n{\n  \"httpProxy\": \"http://127.0.0.1:7890\"\n}\n```\n\n### Advertencias\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `warnings.anthropicExtraUsage` | booleano | `true` | Mostrar una advertencia cuando la autenticación de suscripción de Anthropic pueda utilizar un uso adicional pago |\n\n```json\n{\n  \"warnings\": {\n    \"anthropicExtraUsage\": false\n  }\n}\n```\n\n### Compactación\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `compaction.enabled` | booleano | `true` | Habilitar la autocompactación |\n| `compaction.reserveTokens` | número | `16384` | Tokens reservados para la respuesta LLM |\n| `compaction.keepRecentTokens` | número | `20000` | Tokens recientes para conservar (no resumidos) |\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n### Resumen de sucursales\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `branchSummary.reserveTokens` | número | `16384` | Fichas reservadas para branch summarization |\n| `branchSummary.skipPrompt` | booleano | `false` | Saltar \"¿Resumir rama?\" mensaje en `/tree` navegación (el valor predeterminado es sin resumen) |\n\n### Rever\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `retry.enabled` | booleano | `true` | Habilite el reintento automático a nivel de agente en caso de errores transitorios |\n| `retry.maxRetries` | número | `3` | Número máximo de reintentos a nivel de agente |\n| `retry.baseDelayMs` | número | `2000` | Retraso base para retroceso exponencial a nivel de agente (2s, 4s, 8s) |\n| `retry.provider.timeoutMs` | número | SDK predeterminado | Proveedor/SDK tiempo de espera de solicitud en milisegundos |\n| `retry.provider.maxRetries` | número | `0` | Proveedor/SDK reintentos |\n| `retry.provider.maxRetryDelayMs` | número | `60000` | Retraso máximo solicitado por el servidor antes de fallar (60 s) |\n\nCuando un proveedor solicita un retraso de reintento superior a `retry.provider.maxRetryDelayMs`, la solicitud falla inmediatamente con un error informativo en lugar de esperar en silencio. Configúrelo en `0` para desactivar el límite.\n\nMantenga `retry.provider.maxRetries` en `0` a menos que se necesiten explícitamente reintentos a nivel de proveedor. Configurarlo por encima de `0` puede hacer que SDK/los reintentos del proveedor manejen errores de límite de uso antes de que Pi los vea, lo que puede bloquear al agente hasta que se restablezca la cuota del proveedor en algunas circunstancias.\n\n```json\n{\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3,\n    \"baseDelayMs\": 2000,\n    \"provider\": {\n      \"timeoutMs\": 3600000,\n      \"maxRetries\": 0,\n      \"maxRetryDelayMs\": 60000\n    }\n  }\n}\n```\n\n### Entrega de mensajes\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `steeringMode` | cadena | `\"one-at-a-time\"` | Cómo se envían los mensajes de dirección: `\"all\"` o `\"one-at-a-time\"` |\n| `followUpMode` | cadena | `\"one-at-a-time\"` | Cómo se envían los mensajes de seguimiento: `\"all\"` o `\"one-at-a-time\"` |\n| `transport` | cadena | `\"auto\"` | Transporte preferido para proveedores que admiten múltiples transportes: `\"sse\"`, `\"websocket\"`, `\"websocket-cached\"` o `\"auto\"` |\n| `httpIdleTimeoutMs` | número | `300000` | Tiempo de espera de inactividad del encabezado/cuerpo HTTP en milisegundos, también utilizado por proveedores con tiempos de espera de inactividad de flujo explícitos. Establezca en `0` para desactivar. |\n| `websocketConnectTimeoutMs` | número | `15000` | Tiempo de espera del protocolo de enlace de apertura/conexión de WebSocket en milisegundos para proveedores que admiten transportes de WebSocket. Establezca en `0` para desactivar. |\n\n### Terminales e imágenes\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `terminal.showImages` | booleano | `true` | Mostrar imágenes en la terminal (si es compatible) |\n| `terminal.imageWidthCells` | número | `60` | Ancho de imagen en línea preferido en celdas terminales |\n| `terminal.clearOnShrink` | booleano | `false` | Borrar filas vacías cuando el contenido se reduce (puede causar parpadeo) |\n| `images.autoResize` | booleano | `true` | Cambiar el tamaño de las imágenes a 2000x2000 máx. Se aplica a los archivos adjuntos `@file`, `read` y a las imágenes devueltas por las herramientas |\n| `images.blockImages` | booleano | `false` | Bloquear todas las imágenes para que no se envíen a LLM |\n\n### Caparazón\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `shellPath` | cadena | - | Ruta de shell personalizada (por ejemplo, para Cygwin en Windows); admite un `~` inicial para el directorio de inicio |\n| `shellCommandPrefix` | cadena | - | Prefijo para cada comando bash (por ejemplo, `\"shopt -s expand_aliases\"`) |\n| `npmCommand` | cadena[] | - | Comando argv utilizado para npm operaciones de búsqueda/instalación de paquetes (por ejemplo, `[\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]`) |\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n`npmCommand` se usa para todas las operaciones del administrador de paquetes npm, incluidas instalaciones, desinstalaciones e instalaciones de dependencia dentro de paquetes git. Los paquetes npm de ámbito de usuario se instalan en `~/.pi/agent/npm/`; Los paquetes npm con alcance de proyecto se instalan en `.pi/npm/`. Utilice entradas de estilo argv exactamente como se debe iniciar el proceso. Cuando se configura `npmCommand`, las instalaciones de dependencia de paquetes git usan `install` simple para evitar indicadores específicos de npm en contenedores o administradores de paquetes alternativos.\n\n### Sesiones\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `sessionDir` | cadena | - | Directorio donde se almacenan los archivos de sesión. Acepta rutas absolutas o relativas, más `~`. |\n\n```json\n{ \"sessionDir\": \".pi/sessions\" }\n```\n\nCuando varias fuentes especifican un directorio de sesión, la prioridad es `--session-dir`, `PI_CODING_AGENT_SESSION_DIR` y luego `sessionDir` en settings.json.\n\n### Modelo Ciclismo\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `enabledModels` | cadena[] | - | Patrones de modelo para ciclos Ctrl+P (mismo formato que la bandera `--models` CLI) |\n\n```json\n{\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\", \"gemini-2*\"]\n}\n```\n\n### Markdown\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `markdown.codeBlockIndent` | cadena | `\"  \"` | Sangría para bloques de código |\n| `markdown.mermaid` | cadena | `\"streaming\"` | Modo de representación de sirena: `\"off\"`, `\"final\"` o `\"streaming\"` |\n\n### Recursos\n\nEstas configuraciones definen desde dónde cargar extensiones, habilidades, indicaciones y temas.\n\nLas rutas en `~/.pi/agent/settings.json` se resuelven en relación con `~/.pi/agent`. Las rutas en `.pi/settings.json` se resuelven en relación con `.pi`. Se admiten rutas absolutas y `~`.\n\n| Configuración | Tipo | Por defecto | Descripción |\n|---------|------|---------|-------------|\n| `packages` | formación | `[]` | npm/git paquetes desde los que cargar recursos |\n| `extensions` | cadena[] | `[]` | Rutas o directorios de archivos de extensión locales |\n| `skills` | cadena[] | `[]` | Rutas o directorios de archivos de habilidades locales |\n| `prompts` | cadena[] | `[]` | Rutas o directorios de plantillas de mensajes locales |\n| `themes` | cadena[] | `[]` | Rutas o directorios de archivos de temas locales |\n| `enableSkillCommands` | booleano | `true` | Registrar habilidades como comandos `/skill:name` |\n\nLas matrices admiten patrones globales y exclusiones. Utilice `!pattern` para excluir. Utilice `+path` para forzar la inclusión de una ruta exacta y `-path` para forzar la exclusión de una ruta exacta.\n\n#### paquetes\n\nEl formulario de cadena carga todos los recursos de un paquete:\n\n```json\n{\n  \"packages\": [\"pi-skills\", \"@org/my-extension\"]\n}\n```\n\nEl formulario de objeto filtra qué recursos cargar:\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"pi-skills\",\n      \"skills\": [\"brave-search\", \"transcribe\"],\n      \"extensions\": []\n    }\n  ]\n}\n```\n\nConsulte [packages.md](packages.md) para obtener detalles sobre la administración de paquetes.\n\n## Ejemplo\n\n```json\n{\n  \"defaultProvider\": \"anthropic\",\n  \"defaultModel\": \"claude-sonnet-4-20250514\",\n  \"defaultThinkingLevel\": \"medium\",\n  \"theme\": \"dark\",\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  },\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3\n  },\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\"],\n  \"warnings\": {\n    \"anthropicExtraUsage\": true\n  },\n  \"packages\": [\"pi-skills\"]\n}\n```\n\n## Anulaciones de proyectos\n\nLa configuración del proyecto (`.pi/settings.json`) anula la configuración global. Los objetos anidados se fusionan:\n\n```json\n// ~/.pi/agent/settings.json (global)\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 16384 }\n}\n\n// .pi/settings.json (project)\n{\n  \"compaction\": { \"reserveTokens\": 8192 }\n}\n\n// Result\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 8192 }\n}\n```","sourceFile":"settings.md"},"shell-aliases":{"title":"Alias ​​de Shell","markdown":"Pi ejecuta bash en modo no interactivo (`bash -c`), que no expande los alias de forma predeterminada.\n\nPara habilitar sus alias de shell, agregue a `~/.pi/agent/settings.json`:\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\neval \\\"$(grep '^alias ' ~/.zshrc)\\\"\"\n}\n```\n\nAjuste la ruta (`~/.zshrc`, `~/.bashrc`, etc.) para que coincida con la configuración de su shell.","sourceFile":"shell-aliases.md"},"skills":{"title":"Skills","markdown":"> pi puede crear habilidades. Pídale que cree uno para su caso de uso.\n\n\nSkills son paquetes de capacidades independientes que el agente carga según demanda. Una habilidad proporciona flujos de trabajo especializados, instrucciones de configuración, scripts de ayuda y documentación de referencia para tareas específicas.\n\nPi implementa el [Agent Skills standard](https://agentskills.io/specification), advirtiendo sobre la mayoría de las violaciones pero siendo indulgente. Pi permite que los nombres de las habilidades difieran de su directorio principal aunque el estándar no lo permita; esa regla no es óptima para directorios de habilidades compartidos utilizados en múltiples arneses de agentes.\n\n## Tabla de contenido\n\n- [Locations](#locations)\n- [How Skills Work](#how-skills-work)\n- [Skill Commands](#skill-commands)\n- [Skill Structure](#skill-structure)\n- [Frontmatter](#frontmatter)\n- [Validation](#validation)\n- [Example](#example)\n- [Skill Repositories](#skill-repositories)\n\n## Ubicaciones\n\n> **Seguridad:** Skills puede indicarle al modelo que realice cualquier acción y puede incluir código ejecutable que invoque el modelo. Revise el contenido de las habilidades antes de usarlas.\n\nPi carga habilidades de:\n\n- Global:\n  - `~/.pi/agent/skills/`\n  - `~/.agents/skills/`\n- Proyecto (solo después de que se confíe en el proyecto):\n  - `.pi/skills/`\n  - `.agents/skills/` en `cwd` y directorios ancestrales (hasta la raíz del repositorio de git o la raíz del sistema de archivos cuando no está en un repositorio)\n- Paquetes: `skills/` directorios o `pi.skills` entradas en `package.json`\n- Configuraciones: `skills` matriz con archivos o directorios\n- CLI: `--skill <path>` (repetible, aditivo incluso con `--no-skills`)\n\nReglas de descubrimiento:\n- En `~/.pi/agent/skills/` y `.pi/skills/`, los archivos raíz directos `.md` se descubren como habilidades individuales\n- En todas las ubicaciones de habilidades, los directorios que contienen `SKILL.md` se descubren de forma recursiva\n- En `~/.agents/skills/` y el proyecto `.agents/skills/`, los archivos raíz `.md` se ignoran\n\nDeshabilite el descubrimiento con `--no-skills` (las rutas explícitas `--skill` aún se cargan).\n\n### Usando Skills de otros arneses\n\nPara utilizar habilidades de Claude Code u OpenAI Codex, agregue sus directorios a la configuración:\n\n```json\n{\n  \"skills\": [\n    \"~/.claude/skills\",\n    \"~/.codex/skills\"\n  ]\n}\n```\n\nPara conocer las habilidades de Claude Code a nivel de proyecto, agregue a `.pi/settings.json`:\n\n```json\n{\n  \"skills\": [\"../.claude/skills\"]\n}\n```\n\n## Cómo funciona Skills\n\n1. Al inicio, pi escanea ubicaciones de habilidades y extrae nombres y descripciones.\n2. El indicador del sistema incluye habilidades disponibles en formato XML según [specification](https://agentskills.io/integrate-skills)\n3. Cuando una tarea coincide, el agente usa `read` para cargar el SKILL.md completo (los modelos no siempre hacen esto; use indicaciones o `/skill:name` para forzarlo)\n4. El agente sigue las instrucciones y utiliza rutas relativas para hacer referencia a scripts y activos.\n\nEsta es una divulgación progresiva: solo las descripciones están siempre en contexto, las instrucciones completas se cargan a pedido.\n\n## Comandos de habilidad\n\nSkills registrarse como `/skill:name` comandos:\n\n```bash\n/skill:brave-search           # Load and execute the skill\n/skill:pdf-tools extract      # Load skill with arguments\n```\n\nLos argumentos después del comando se agregan al contenido de la habilidad como `User: <args>`.\n\nAlternar comandos de habilidad a través de `/settings` en modo interactivo o en `settings.json`:\n\n```json\n{\n  \"enableSkillCommands\": true\n}\n```\n\n## Estructura de habilidades\n\nUna habilidad es un directorio con un archivo `SKILL.md`. Todo lo demás es de forma libre.\n\n```\nmy-skill/\n├── SKILL.md              # Required: frontmatter + instructions\n├── scripts/              # Helper scripts\n│   └── process.sh\n├── references/           # Detailed docs loaded on-demand\n│   └── api-reference.md\n└── assets/\n    └── template.json\n```\n\n### Formato HABILIDAD.md\n\n````markdown\n---\nname: my-skill\ndescription: What this skill does and when to use it. Be specific.\n---\n\n# My Skill\n\n## Setup\n\nRun once before first use:\n```bash\ncd /ruta/a/skill && npm instalar\n```\n\n## Usage\n\n```bash\n./scripts/process.sh <input>\n```\n````\n\nUtilice rutas relativas del directorio de habilidades:\n\n```markdown\nSee [the reference guide](references/REFERENCE.md) for details.\n```\n\n## Frontasunto\n\nSegún el [Agent Skills specification](https://agentskills.io/specification#frontmatter-required):\n\n| Campo | Requerido | Descripción |\n|-------|----------|-------------|\n| `name` | Sí | Máximo 64 caracteres. Minúsculas a-z, 0-9, guiones. A diferencia del estándar, Pi no requiere que esto coincida con el directorio principal porque ese requisito estándar no es óptimo para los directorios de habilidades compartidos. |\n| `description` | Sí | Máximo 1024 caracteres. Qué hace la habilidad y cuándo usarla. |\n| `license` | No | Nombre de la licencia o referencia al archivo incluido. |\n| `compatibility` | No | Máximo 500 caracteres. Requisitos ambientales. |\n| `metadata` | No | Mapeo arbitrario de valores clave. |\n| `allowed-tools` | No | Lista delimitada por espacios de herramientas preaprobadas (experimental). |\n| `disable-model-invocation` | No | Cuando `true`, la habilidad se oculta del indicador del sistema. Los usuarios deben utilizar `/skill:name`. |\n\n### Reglas de nombres\n\n- 1-64 caracteres\n- Letras minúsculas, números y guiones únicamente.\n- Sin guiones iniciales o finales\n- Sin guiones consecutivos\nPi no requiere que el nombre coincida con el directorio principal. El estándar Agente Skills sí lo hace, pero ese requisito no es óptimo para directorios de habilidades compartidos utilizados por múltiples herramientas.\n\nVálido: `pdf-processing`, `data-analysis`, `code-review`\nNo válido: `PDF-Processing`, `-pdf`, `pdf--processing`\n\n### Descripción Mejores prácticas\n\nLa descripción determina cuándo el agente carga la habilidad. Sea específico.\n\nBien:\n```yaml\ndescription: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents.\n```\n\nPobre:\n```yaml\ndescription: Helps with PDFs.\n```\n\n## Validación\n\nPi valida las habilidades según el estándar del Agente Skills. La mayoría de los problemas generan advertencias pero aún cargan la habilidad:\n\n- El nombre supera los 64 caracteres o contiene caracteres no válidos\n- El nombre comienza/termina con guión o tiene guiones consecutivos\n- La descripción supera los 1024 caracteres.\n\nLos campos desconocidos se ignoran.\n\n**Excepción:** Skills a los que les falta una descripción no se cargan.\n\nLas colisiones de nombres (el mismo nombre en diferentes ubicaciones) advierten y mantienen la primera habilidad encontrada.\n\n## Ejemplo\n\n```\nbrave-search/\n├── SKILL.md\n├── search.js\n└── content.js\n```\n\n**HABILIDAD.md:**\n````markdown\n---\nname: brave-search\ndescription: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content.\n---\n\n# Brave Search\n\n## Setup\n\n```bash\ncd /ruta/a/brave-search && npm instalar\n```\n\n## Search\n\n```bash\n./search.js \"consulta\" # Búsqueda básica\n./search.js \"query\" --content # Incluir contenido de la página\n```\n\n## Extract Page Content\n\n```bash\n./content.js https://ejemplo.com\n```\n````\n\n## Repositorios de habilidades\n\n- [Anthropic Skills](https://github.com/anthropics/skills) - Procesamiento de documentos (docx, pdf, pptx, xlsx), desarrollo web\n- [Pi Skills](https://github.com/badlogic/pi-skills) - Búsqueda web, automatización del navegador, Google APIs, transcripción","sourceFile":"skills.md"},"terminal-setup":{"title":"Configuración de terminales","markdown":"Pi usa [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) para una detección confiable de la tecla modificadora. La mayoría de los terminales modernos admiten este protocolo, pero algunos requieren configuración.\n\n## Gatito, iTerm2\n\nTrabaje fuera de la caja.\n\n## Terminal de Apple\n\nPi habilita informes clave mejorados cuando estén disponibles. Si Terminal.app aún envía un retorno simple para `Shift+Enter`, pi usa un modificador alternativo de macOS local para tratar ese retorno como `Shift+Enter`.\n\nEste respaldo solo funciona cuando pi se ejecuta en la misma Mac que Terminal.app. No puede detectar el teclado local a través del control remoto SSH.\n\n## fantasmal\n\nAgregue a su configuración de Ghostty (`~/Library/Application Support/com.mitchellh.ghostty/config` en macOS, `~/.config/ghostty/config` en Linux):\n\n```\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\nEs posible que las versiones anteriores de Claude Code hayan agregado este mapeo Ghostty:\n\n```\nkeybind = shift+enter=text:\\n\n```\n\nEse mapeo envía un byte de avance de línea sin formato. Dentro de pi, eso es indistinguible de `Ctrl+J`, por lo que tmux y pi ya no ven un evento clave `shift+enter` real.\n\nSi Claude Code 2.x o posterior es la única razón por la que agregó ese mapeo, puede eliminarlo, a menos que desee usar Claude Code en tmux, donde todavía requiere ese mapeo Ghostty.\n\nPi vincula `Ctrl+J` como alias de nueva línea predeterminado, por lo que `Shift+Enter` sigue trabajando en tmux a través de esa reasignación sin configuración pi adicional.\n\n## WezTérmino\n\nWezTerm generalmente funciona de inmediato para `Shift+Enter` a través de xtermmodifyOtherKeys. Para utilizar el protocolo de teclado Kitty explícitamente, cree `~/.wezterm.lua`:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\nEn macOS, WezTerm vincula `Option+Enter` a pantalla completa de forma predeterminada. Para usar `Option+Enter` para la cola de seguimiento de pi, agregue esta anulación de clave:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.keys = {\n  {\n    key = 'Enter',\n    mods = 'ALT',\n    action = wezterm.action.SendString('\\x1b[13;3u'),\n  },\n}\nreturn config\n```\n\nSi ya tiene una tabla `config.keys`, agréguele la entrada.\n\nEn WSL, WezTerm puede requerir un cursor de hardware visible para el posicionamiento de la ventana candidata de IME. Si los candidatos de CJK IME no siguen el cursor de texto, configure `PI_HARDWARE_CURSOR=1` antes de ejecutar pi o configure `showHardwareCursor` en `true` en la configuración.\n\n## Alacritty\n\nAlacritty generalmente funciona de inmediato para `Shift+Enter`. En macOS, `Option+Enter` puede llegar como `Enter` simple. Para usar `Option+Enter` para la cola de seguimiento de pi, agregue a `~/.config/alacritty/alacritty.toml`:\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\nReinicie Alacritty después de cambiar la configuración.\n\n## Código VS (Terminal Integrado)\n\nVS Code 1.109.5 y versiones más recientes habilitan el protocolo de teclado Kitty en el terminal integrado de forma predeterminada, por lo que `Shift+Enter` debería funcionar de inmediato.\n\nLas versiones de VS Code anteriores a 1.109.5 necesitan una combinación de teclas de terminal explícita para `Shift+Enter`.\n\n`keybindings.json` ubicaciones:\n- MacOS: `~/Library/Application Support/Code/User/keybindings.json`\n- Linux: `~/.config/Code/User/keybindings.json`\n- Ventanas: `%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\nAñadir a `keybindings.json`:\n\n```json\n{\n  \"key\": \"shift+enter\",\n  \"command\": \"workbench.action.terminal.sendSequence\",\n  \"args\": { \"text\": \"\\u001b[13;2u\" },\n  \"when\": \"terminalFocus\"\n}\n```\n\n## Terminal de Windows\n\nAgregue a `settings.json` (Ctrl+Shift+, o Configuración → Abrir archivo JSON) para reenviar las teclas Enter modificadas que usa pi:\n\n```json\n{\n  \"actions\": [\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;2u\" },\n      \"keys\": \"shift+enter\"\n    },\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;3u\" },\n      \"keys\": \"alt+enter\"\n    }\n  ]\n}\n```\n\n- `Shift+Enter` inserta una nueva línea.\n- Windows Terminal vincula `Alt+Enter` a pantalla completa de forma predeterminada. Eso evita que pi reciba `Alt+Enter` para la cola de seguimiento.\n- Reasignar `Alt+Enter` a `sendInput` reenvía el acorde clave real a pi.\n\nSi ya tiene una matriz `actions`, agréguele los objetos. Si el antiguo comportamiento de pantalla completa persiste, cierre completamente y vuelva a abrir Windows Terminal.\n\n## xfce4-terminal, terminador\n\nEstos terminales tienen soporte limitado para secuencias de escape. Las teclas Enter modificadas como `Ctrl+Enter` y `Shift+Enter` no se pueden distinguir de las simples `Enter`, lo que impide que funcionen combinaciones de teclas personalizadas como `submit: [\"ctrl+enter\"]`.\n\nPara obtener la mejor experiencia, utilice un terminal que admita el protocolo de teclado Kitty:\n- [Kitty](https://sw.kovidgoyal.net/kitty/)\n- [Ghostty](https://ghostty.org/)\n- [WezTerm](https://wezfurlong.org/wezterm/)\n- [iTerm2](https://iterm2.com/)\n- [Alacritty](https://github.com/alacritty/alacritty) (requiere compilación con soporte para el protocolo Kitty)\n\n## IntelliJ IDEA (Terminal integrada)\n\nEl terminal incorporado tiene soporte limitado para secuencias de escape. Shift+Enter no se puede distinguir de Enter en la terminal de IntelliJ.\n\nSi desea que el cursor de hardware esté visible, configure `PI_HARDWARE_CURSOR=1` antes de ejecutar pi (deshabilitado de forma predeterminada por compatibilidad).\n\nConsidere utilizar un emulador de terminal dedicado para obtener la mejor experiencia.","sourceFile":"terminal-setup.md"},"termux":{"title":"Termux (Android) Configuración","markdown":"Pi se ejecuta en Android a través de [Termux](https://termux.dev/), un emulador de terminal y entorno Linux para Android.\n\n## Requisitos previos\n\n1. Instale [Termux](https://github.com/termux/termux-app#installation) desde GitHub o F-Droid (no en Google Play, esa versión está obsoleta)\n2. Instale [Termux:API](https://github.com/termux/termux-api#installation) desde GitHub o F-Droid para integraciones con el portapapeles y otros dispositivos\n\n## Instalación\n\n```bash\n# Update packages\npkg update && pkg upgrade\n\n# Install dependencies\npkg install nodejs termux-api git\n\n# Install pi\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\n# Create config directory\nmkdir -p ~/.pi/agent\n\n# Run pi\npi\n```\n\n## Soporte para portapapeles\n\nLas operaciones del portapapeles utilizan `termux-clipboard-set` y `termux-clipboard-get` cuando se ejecutan en Termux. La aplicación Termux:API debe estar instalada para que funcionen.\n\nEl portapapeles de imágenes no es compatible con Termux (la función de pegado de imágenes `ctrl+v` no funcionará).\n\n## Ejemplo AGENTES.md para Termux\n\nCree `~/.pi/agent/AGENTS.md` para ayudar al agente a comprender el entorno Termux:\n\n````markdown\n# Agent Environment: Termux on Android\n\n## Location\n- **OS**: Android (Termux terminal emulator)\n- **Home**: `/data/data/com.termux/files/home`\n- **Prefix**: `/data/data/com.termux/files/usr`\n- **Shared storage**: `/storage/emulated/0` (Downloads, Documents, etc.)\n\n## Opening URLs\n```bash\ntermux-open-url \"https://ejemplo.com\"\n```\n\n## Opening Files\n```bash\ntermux-open file.pdf # Se abre con la aplicación predeterminada\ntermux-open --chooser image.jpg # Elige aplicación\n```\n\n## Clipboard\n```bash\ntermux-portapapeles-set \"texto\" # Copiar\ntermux-portapapeles-get # Pegar\n```\n\n## Notifications\n```bash\ntermux-notificación -t \"Título\" -c \"Contenido\"\n```\n\n## Device Info\n```bash\ntermux-battery-status # Información de la batería\ntermux-wifi-connectioninfo # Información WiFi\ntermux-telephony-deviceinfo # Información del dispositivo\n```\n\n## Sharing\n```bash\ntermux-share -a enviar archivo.txt # Compartir archivo\n```\n\n## Other Useful Commands\n```bash\ntermux-toast \"mensaje\" # Ventana emergente de brindis rápido\ntermux-vibrate # Dispositivo de vibración\ntermux-tts-speak \"hola\" # Texto a voz\ntermux-camera-photo out.jpg # Tomar foto\n```\n\n## Notes\n- Termux:API app must be installed for `termux-*` commands\n- Use `pkg install termux-api` for the command-line tools\n- Storage permission needed for `/storage/emulated/0` access\n````\n\n## Limitaciones\n\n- **Sin portapapeles de imágenes**: Termux portapapeles API solo admite texto\n- **Sin archivos binarios nativos**: algunas dependencias nativas opcionales (como el módulo del portapapeles) no están disponibles en Android ARM64 y se omiten durante la instalación.\n- **Acceso al almacenamiento**: para acceder a los archivos en `/storage/emulated/0` (Descargas, etc.), ejecute `termux-setup-storage` una vez para otorgar permisos\n\n## Solución de problemas\n\n### Portapapeles no funciona\n\nAsegúrese de que ambas aplicaciones estén instaladas:\n1. Termux (de GitHub o F-Droid)\n2. Termux:API (de GitHub o F-Droid)\n\nLuego instale las herramientas CLI:\n```bash\npkg install termux-api\n```\n\n### Permiso denegado para almacenamiento compartido\n\nEjecute una vez para otorgar permisos de almacenamiento:\n```bash\ntermux-setup-storage\n```\n\n### Node.js problemas de instalación\n\nSi npm falla, intente borrar el caché:\n```bash\nnpm cache clean --force\n```","sourceFile":"termux.md"},"themes":{"title":"Temas","markdown":"> pi puede crear temas. Pídale que cree uno para su configuración.\n\n\nLos temas son archivos JSON que definen colores para TUI.\n\n## Tabla de contenido\n\n- [Locations](#locations)\n- [Selecting a Theme](#selecting-a-theme)\n- [Creating a Custom Theme](#creating-a-custom-theme)\n- [Theme Format](#theme-format)\n- [Color Tokens](#color-tokens)\n- [Color Values](#color-values)\n- [Tips](#tips)\n\n## Ubicaciones\n\nPi carga temas de:\n\n- Incorporado: `dark`, `light`\n- Global: `~/.pi/agent/themes/*.json`\n- Proyecto: `.pi/themes/*.json` (solo después de que se confíe en el proyecto)\n- Paquetes: `themes/` directorios o `pi.themes` entradas en `package.json`\n- Configuraciones: `themes` matriz con archivos o directorios\n- CLI: `--theme <path>` (repetible)\n\nDesactive el descubrimiento con `--no-themes`.\n\n## Seleccionar un tema\n\nSeleccione un tema a través de `/settings` o en `settings.json`:\n\n```json\n{\n  \"theme\": \"my-theme\"\n}\n```\n\nEn la primera ejecución, pi detecta el fondo de su terminal y su valor predeterminado es `dark` o `light`.\n\n## Crear un tema personalizado\n\n1. Crea un archivo de tema:\n\n```bash\nmkdir -p ~/.pi/agent/themes\nvim ~/.pi/agent/themes/my-theme.json\n```\n\n2. Defina el tema con todos los colores requeridos (ver [Color Tokens](#color-tokens)):\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"primary\": \"#00aaff\",\n    \"secondary\": 242\n  },\n  \"colors\": {\n    \"accent\": \"primary\",\n    \"border\": \"primary\",\n    \"borderAccent\": \"#00ffff\",\n    \"borderMuted\": \"secondary\",\n    \"success\": \"#00ff00\",\n    \"error\": \"#ff0000\",\n    \"warning\": \"#ffff00\",\n    \"muted\": \"secondary\",\n    \"dim\": 240,\n    \"text\": \"\",\n    \"thinkingText\": \"secondary\",\n    \"selectedBg\": \"#2d2d30\",\n    \"scrollbarThumb\": \"#555566\",\n    \"userMessageBg\": \"#2d2d30\",\n    \"userMessageText\": \"\",\n    \"customMessageBg\": \"#2d2d30\",\n    \"customMessageText\": \"\",\n    \"customMessageLabel\": \"primary\",\n    \"toolPendingBg\": \"#1e1e2e\",\n    \"toolSuccessBg\": \"#1e2e1e\",\n    \"toolErrorBg\": \"#2e1e1e\",\n    \"toolTitle\": \"primary\",\n    \"toolOutput\": \"\",\n    \"mdHeading\": \"#ffaa00\",\n    \"mdLink\": \"primary\",\n    \"mdLinkUrl\": \"secondary\",\n    \"mdCode\": \"#00ffff\",\n    \"mdCodeBlock\": \"\",\n    \"mdCodeBlockBorder\": \"secondary\",\n    \"mdQuote\": \"secondary\",\n    \"mdQuoteBorder\": \"secondary\",\n    \"mdHr\": \"secondary\",\n    \"mdListBullet\": \"#00ffff\",\n    \"toolDiffAdded\": \"#00ff00\",\n    \"toolDiffRemoved\": \"#ff0000\",\n    \"toolDiffContext\": \"secondary\",\n    \"syntaxComment\": \"secondary\",\n    \"syntaxKeyword\": \"primary\",\n    \"syntaxFunction\": \"#00aaff\",\n    \"syntaxVariable\": \"#ffaa00\",\n    \"syntaxString\": \"#00ff00\",\n    \"syntaxNumber\": \"#ff00ff\",\n    \"syntaxType\": \"#00aaff\",\n    \"syntaxOperator\": \"primary\",\n    \"syntaxPunctuation\": \"secondary\",\n    \"thinkingOff\": \"secondary\",\n    \"thinkingMinimal\": \"primary\",\n    \"thinkingLow\": \"#00aaff\",\n    \"thinkingMedium\": \"#00ffff\",\n    \"thinkingHigh\": \"#ff00ff\",\n    \"thinkingXhigh\": \"#ff0000\",\n    \"thinkingMax\": \"#ff0088\",\n    \"bashMode\": \"#ffaa00\"\n  }\n}\n```\n\n3. Seleccione el tema mediante `/settings`.\n\n**Recarga en caliente:** Cuando editas el archivo de tema personalizado actualmente activo, pi lo recarga automáticamente para obtener información visual inmediata.\n\n## Formato del tema\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"blue\": \"#0066cc\",\n    \"gray\": 242\n  },\n  \"colors\": {\n    \"accent\": \"blue\",\n    \"muted\": \"gray\",\n    \"text\": \"\",\n    ...\n  }\n}\n```\n\n- `name` es obligatorio, debe ser único y no debe contener `/`.\n- `vars` es opcional. Defina colores reutilizables aquí y luego haga referencia a ellos en `colors`.\n- `colors` debe definir los 51 tokens requeridos. `thinkingMax` es opcional y vuelve a ser `thinkingXhigh`; `scrollbarThumb` es opcional y vuelve a ser `selectedBg`.\n\nEl campo `$schema` permite la validación y el autocompletado del editor.\n\n## Fichas de colores\n\nCada tema debe definir los 51 tokens de color requeridos. `thinkingMax` y `scrollbarThumb` son opcionales por compatibilidad con temas existentes; cuando se omiten, usan `thinkingXhigh` y `selectedBg`, respectivamente.\n\n### Interfaz de usuario principal (11 colores)\n\n| Simbólico | Objetivo |\n|-------|---------|\n| `accent` | Acento principal (logotipo, elementos seleccionados, cursor) |\n| `border` | Fronteras normales |\n| `borderAccent` | Bordes resaltados |\n| `borderMuted` | Fronteras sutiles (editor) |\n| `success` | Estados de éxito |\n| `error` | Estados de error |\n| `warning` | Estados de advertencia |\n| `muted` | Texto secundario |\n| `dim` | texto terciario |\n| `text` | Texto predeterminado (normalmente `\"\"`) |\n| `thinkingText` | Texto de bloque de pensamiento |\n\n### Fondos y contenido (11 obligatorios, 1 opcional)\n\n| Simbólico | Objetivo |\n|-------|---------|\n| `selectedBg` | Fondo de línea seleccionada |\n| `scrollbarThumb` | Fondo del pulgar de la barra de desplazamiento de pantalla completa; opcional, vuelve a `selectedBg` |\n| `userMessageBg` | Fondo del mensaje de usuario |\n| `userMessageText` | Texto del mensaje de usuario |\n| `customMessageBg` | Fondo del mensaje de extensión |\n| `customMessageText` | Texto del mensaje de extensión |\n| `customMessageLabel` | Etiqueta de mensaje de extensión |\n| `toolPendingBg` | Caja de herramientas (pendiente) |\n| `toolSuccessBg` | Caja de herramientas (éxito) |\n| `toolErrorBg` | Caja de herramientas (error) |\n| `toolTitle` | Título de la herramienta |\n| `toolOutput` | Texto de salida de herramienta |\n\n### Markdown (10 colores)\n\n| Simbólico | Objetivo |\n|-------|---------|\n| `mdHeading` | Encabezamientos |\n| `mdLink` | Texto del enlace |\n| `mdLinkUrl` | URL del enlace |\n| `mdCode` | código en línea |\n| `mdCodeBlock` | Contenido del bloque de código |\n| `mdCodeBlockBorder` | Vallas de bloques de código |\n| `mdQuote` | Texto de cita en bloque |\n| `mdQuoteBorder` | Borde de cita en bloque |\n| `mdHr` | regla horizontal |\n| `mdListBullet` | Lista de viñetas |\n\n### Diferencias de herramientas (3 colores)\n\n| Simbólico | Objetivo |\n|-------|---------|\n| `toolDiffAdded` | Líneas agregadas |\n| `toolDiffRemoved` | Líneas eliminadas |\n| `toolDiffContext` | Líneas de contexto |\n\n### Resaltado de sintaxis (9 colores)\n\n| Simbólico | Objetivo |\n|-------|---------|\n| `syntaxComment` | Comentarios |\n| `syntaxKeyword` | Palabras clave |\n| `syntaxFunction` | Nombres de funciones |\n| `syntaxVariable` | variables |\n| `syntaxString` | Instrumentos de cuerda |\n| `syntaxNumber` | Números |\n| `syntaxType` | Tipos |\n| `syntaxOperator` | Operadores |\n| `syntaxPunctuation` | Puntuación |\n\n### Bordes del nivel de pensamiento (6 obligatorios, 1 opcional)\n\nColores del borde del editor que indican el nivel de pensamiento (jerarquía visual de sutil a prominente):\n\n| Simbólico | Objetivo |\n|-------|---------|\n| `thinkingOff` | Pensando en |\n| `thinkingMinimal` | Pensamiento mínimo |\n| `thinkingLow` | pensamiento bajo |\n| `thinkingMedium` | Pensamiento medio |\n| `thinkingHigh` | pensamiento elevado |\n| `thinkingXhigh` | Pensamiento extra elevado |\n| `thinkingMax` | Pensamiento máximo; opcional, vuelve a `thinkingXhigh` |\n\n### Modo Bash (1 color)\n\n| Simbólico | Objetivo |\n|-------|---------|\n| `bashMode` | Borde del editor en modo bash (prefijo `!`) |\n\n### Exportación HTML (opcional)\n\nLa sección `export` controla los colores para la salida HTML `/export`. Si se omite, los colores se derivan de `userMessageBg`.\n\n```json\n{\n  \"export\": {\n    \"pageBg\": \"#18181e\",\n    \"cardBg\": \"#1e1e24\",\n    \"infoBg\": \"#3c3728\"\n  }\n}\n```\n\n## Valores de color\n\nSe admiten cuatro formatos:\n\n| Formato | Ejemplo | Descripción |\n|--------|---------|-------------|\n| Maleficio | `\"#ff0000\"` | RGB hexadecimal de 6 dígitos |\n| 256 colores | `39` | Índice de paleta de 256 colores xterm (0-255) |\n| Variable | `\"primary\"` | Referencia a una entrada `vars` |\n| Por defecto | `\"\"` | Color predeterminado del terminal |\n\n### Paleta de 256 colores\n\n- `0-15`: Colores ANSI básicos (depende del terminal)\n- `16-231`: cubo RGB de 6×6×6 (`16 + 36×R + 6×G + B` donde R,G,B son 0-5)\n- `232-255`: rampa en escala de grises\n\n### Compatibilidad de terminales\n\nPi utiliza colores RGB de 24 bits. La mayoría de los terminales modernos lo admiten (iTerm2, Kitty, WezTerm, Windows Terminal, VS Code). Para terminales más antiguos que solo admiten 256 colores, pi vuelve a la aproximación más cercana.\n\nVerifique el soporte de color verdadero:\n\n```bash\necho $COLORTERM  # Should output \"truecolor\" or \"24bit\"\n```\n\n## Consejos\n\n**Terminales oscuros:** Utilice colores brillantes y saturados con mayor contraste.\n\n**Terminales claros:** Utilice colores más oscuros y apagados con menor contraste.\n\n**Armonía de color:** Comience con una paleta base (Nord, Gruvbox, Tokyo Night), defínala en `vars` y haga referencia de manera consistente.\n\n**Prueba:** Verifique su tema con diferentes tipos de mensajes, estados de herramientas, contenido de rebajas y texto largo y ajustado.\n\n**Código VS:** Establezca `terminal.integrated.minimumContrastRatio` en `1` para obtener colores precisos.\n\n## Ejemplos\n\nVea los temas integrados:\n- [dark.json](../src/modes/interactive/theme/dark.json)\n- [light.json](../src/modes/interactive/theme/light.json)","sourceFile":"themes.md"},"tmux":{"title":"tmux Configuración","markdown":"Pi funciona dentro de tmux, pero tmux elimina la información modificadora de ciertas teclas de forma predeterminada. Sin configuración, `Shift+Enter` y `Ctrl+Enter` suelen ser indistinguibles del `Enter` simple.\n\n## Configuración recomendada\n\nAñadir a `~/.tmux.conf`:\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\nLuego reinicie tmux completamente:\n\n```bash\ntmux kill-server\ntmux\n```\n\nPi solicita informes de teclas extendidos automáticamente cuando el protocolo de teclado Kitty no está disponible. Con `extended-keys-format csi-u`, tmux reenvía claves modificadas en formato CSI-u, que es la configuración más confiable. La opción `extended-keys-format` requiere tmux 3.5 o posterior.\n\n## Por qué se recomienda `csi-u`\n\nCon solo:\n\n```tmux\nset -g extended-keys on\n```\n\ntmux por defecto es `extended-keys-format xterm`. Cuando una aplicación solicita informes de claves ampliados, las claves modificadas se reenvían en formato xterm `modifyOtherKeys`, como por ejemplo:\n\n- `Ctrl+C` → `\\x1b[27;5;99~`\n- `Ctrl+D` → `\\x1b[27;5;100~`\n- `Ctrl+Enter` → `\\x1b[27;5;13~`\n\nCon `extended-keys-format csi-u`, se reenvían las mismas claves que:\n\n- `Ctrl+C` → `\\x1b[99;5u`\n- `Ctrl+D` → `\\x1b[100;5u`\n- `Ctrl+Enter` → `\\x1b[13;5u`\n\nPi admite ambos formatos, pero `csi-u` es la configuración tmux recomendada.\n\n## Lo que esto soluciona\n\nSin las teclas extendidas tmux, las teclas Enter modificadas colapsan en secuencias heredadas:\n\n| Llave | Sin teclas externas | Con `csi-u` |\n|-----|-----------------|--------------|\n| Ingresar | `\\r` | `\\r` |\n| Mayús+Entrar | `\\r` | `\\x1b[13;2u` |\n| Ctrl+Entrar | `\\r` | `\\x1b[13;5u` |\n| Alt/Opción+Intro | `\\x1b\\r` | `\\x1b[13;3u` |\n\nEsto afecta las combinaciones de teclas predeterminadas (`Enter` para enviar, `Shift+Enter` para nueva línea) y cualquier combinación de teclas personalizada que utilice Enter modificado.\n\n## Requisitos\n\n- tmux 3.5 o posterior para `extended-keys-format csi-u` (ejecute `tmux -V` para verificar)\n- Un emulador de terminal que admite claves extendidas (Ghostty, Kitty, iTerm2, WezTerm, Windows Terminal)\n\nCon tmux 3.2 a 3.4, omita `extended-keys-format csi-u`; Pi todavía admite el formato xterm `modifyOtherKeys` predeterminado de tmux.","sourceFile":"tmux.md"},"tui":{"title":"TUI Componentes","markdown":"> pi puede crear componentes TUI. Pídale que cree uno para su caso de uso.\n\n\nExtensions y las herramientas personalizadas pueden representar componentes TUI personalizados para interfaces de usuario interactivas. Esta página cubre el sistema de componentes y los bloques de construcción disponibles.\n\n**Fuente:** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)\n\n## Interfaz de componente\n\nTodos los componentes implementan:\n\n```typescript\ninterface Component {\n  render(width: number): string[];\n  handleInput?(data: string): void;\n  wantsKeyRelease?: boolean;\n  invalidate(): void;\n}\n```\n\n| Método | Descripción |\n|--------|-------------|\n| `render(width)` | Devuelve una matriz de cadenas (una por línea). Cada línea **no debe exceder `width`**. |\n| `handleInput?(data)` | Reciba entradas del teclado cuando el componente esté enfocado. |\n| `wantsKeyRelease?` | Si es verdadero, el componente recibe eventos de liberación de claves (protocolo Kitty). Valor predeterminado: falso. |\n| `invalidate()` | Borrar el estado de renderizado en caché. Pidió cambios de tema. |\n\nEl TUI agrega un reinicio completo de SGR y un reinicio de OSC 8 al final de cada línea renderizada. Los estilos no cruzan líneas. Si emite texto de varias líneas con estilo, vuelva a aplicar estilos por línea o use `wrapTextWithAnsi()` para que los estilos se conserven para cada línea ajustada.\n\n## Interfaz enfocable (soporte IME)\n\nLos componentes que muestran un cursor de texto y necesitan compatibilidad con IME (Editor de métodos de entrada) deben implementar la interfaz `Focusable`:\n\n```typescript\nimport { CURSOR_MARKER, type Component, type Focusable } from \"@earendil-works/pi-tui\";\n\nclass MyInput implements Component, Focusable {\n  focused: boolean = false;  // Set by TUI when focus changes\n  \n  render(width: number): string[] {\n    const marker = this.focused ? CURSOR_MARKER : \"\";\n    // Emit marker right before the fake cursor\n    return [`> ${beforeCursor}${marker}\\x1b[7m${atCursor}\\x1b[27m${afterCursor}`];\n  }\n}\n```\n\nCuando un componente `Focusable` tiene foco, TUI:\n1. Establece `focused = true` en el componente\n2. Los escaneos generaron resultados para `CURSOR_MARKER` (una secuencia de escape APC de ancho cero)\n3. Coloca el cursor del terminal de hardware en esa ubicación\n4. Muestra el cursor de hardware solo cuando `showHardwareCursor` está habilitado\n\nEl cursor permanece oculto de forma predeterminada. Esto mantiene la representación del cursor falso y al mismo tiempo posiciona el cursor de hardware para terminales que rastrean ventanas candidatas de IME con cursores ocultos. Algunos terminales requieren un cursor de hardware visible para el posicionamiento de IME; habilítelo con `showHardwareCursor`, `setShowHardwareCursor(true)` o `PI_HARDWARE_CURSOR=1`. Los componentes integrados `Editor` y `Input` ya implementan esta interfaz.\n\n### Componentes de contenedor con entradas integradas\n\nCuando un componente contenedor (diálogo, selector, etc.) contiene un hijo `Input` o `Editor`, el contenedor debe implementar `Focusable` y propagar el estado de enfoque al hijo. De lo contrario, el cursor de hardware no se colocará correctamente para la entrada de IME.\n\n```typescript\nimport { Container, type Focusable, Input } from \"@earendil-works/pi-tui\";\n\nclass SearchDialog extends Container implements Focusable {\n  private searchInput: Input;\n\n  // Focusable implementation - propagate to child input for IME cursor positioning\n  private _focused = false;\n  get focused(): boolean {\n    return this._focused;\n  }\n  set focused(value: boolean) {\n    this._focused = value;\n    this.searchInput.focused = value;\n  }\n\n  constructor() {\n    super();\n    this.searchInput = new Input();\n    this.addChild(this.searchInput);\n  }\n}\n```\n\nSin esta propagación, escribir con un IME (chino, japonés, coreano, etc.) mostrará la ventana del candidato en la posición incorrecta en la pantalla.\n\n## Usando componentes\n\n**En extensiones** vía `ctx.ui.custom()`:\n\n```typescript\npi.on(\"session_start\", async (_event, ctx) => {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n});\n```\n\n**En herramientas personalizadas** vía `ctx.ui.custom()`:\n\n```typescript\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n  // Use result...\n}\n```\n\n## Superposiciones\n\nLas superposiciones representan componentes sobre el contenido existente sin borrar la pantalla. Pase `{ overlay: true }` a `ctx.ui.custom()`:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyDialog({ onClose: done }),\n  { overlay: true }\n);\n```\n\nPara posicionamiento y tamaño, use `overlayOptions`:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new SidePanel({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: {\n      // Size: number or percentage string\n      width: \"50%\",          // 50% of terminal width\n      minWidth: 40,          // minimum 40 columns\n      maxHeight: \"80%\",      // max 80% of terminal height\n\n      // Position: anchor-based (default: \"center\")\n      anchor: \"right-center\", // 9 positions: center, top-left, top-center, etc.\n      offsetX: -2,            // offset from anchor\n      offsetY: 0,\n\n      // Or percentage/absolute positioning\n      row: \"25%\",            // 25% from top\n      col: 10,               // column 10\n\n      // Margins\n      margin: 2,             // all sides, or { top, right, bottom, left }\n\n      // Responsive: hide on narrow terminals\n      visible: (termWidth, termHeight) => termWidth >= 80,\n    },\n    // Get handle for programmatic focus and visibility control\n    onHandle: (handle) => {\n      // handle.focus() - focus this overlay and bring it to the visual front\n      // handle.unfocus() - release input to normal fallback\n      // handle.unfocus({ target }) - release input to a specific component or null\n      // handle.setHidden(true/false) - toggle visibility\n      // handle.hide() - permanently remove\n    },\n  }\n);\n```\n\n### Enfoque de superposición\n\nUna superposición visible enfocada mantiene la propiedad de las entradas en la interfaz de usuario temporal sin superposición. Si una superposición abre otro componente `ctx.ui.custom()` sin `{ overlay: true }`, esa interfaz de usuario de reemplazo recibe información mientras está activa; cuando se cierra, la superposición enfocada puede recuperar la entrada.\n\nUtilice `handle.unfocus()` cuando una superposición visible deba dejar de poseer entradas y dejar que TUI vuelva a otra superposición de captura visible o al objetivo de enfoque anterior. Utilice `handle.unfocus({ target })` cuando un componente específico deba recibir información mientras la superposición permanece visible. Pasar `{ target: null }` intencionalmente no deja ningún componente enfocado hasta que se vuelva a establecer el enfoque.\n\n### Ciclo de vida de superposición\n\nLos componentes superpuestos se eliminan cuando están cerrados. No reutilice referencias: cree instancias nuevas:\n\n```typescript\n// Wrong - stale reference\nlet menu: MenuComponent;\nawait ctx.ui.custom((_, __, ___, done) => {\n  menu = new MenuComponent(done);\n  return menu;\n}, { overlay: true });\nsetActiveComponent(menu);  // Disposed\n\n// Correct - re-call to re-show\nconst showMenu = () => ctx.ui.custom((_, __, ___, done) => \n  new MenuComponent(done), { overlay: true });\n\nawait showMenu();  // First show\nawait showMenu();  // \"Back\" = just call again\n```\n\nConsulte [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) para obtener ejemplos completos que cubren anclajes, márgenes, apilamiento, visibilidad receptiva y animación.\n\n## Componentes incorporados\n\nImportar desde `@earendil-works/pi-tui`:\n\n```typescript\nimport { Text, Box, Container, Spacer, Markdown } from \"@earendil-works/pi-tui\";\n```\n\n### Texto\n\nTexto de varias líneas con ajuste de palabras.\n\n```typescript\nconst text = new Text(\n  \"Hello World\",    // content\n  1,                // paddingX (default: 1)\n  1,                // paddingY (default: 1)\n  (s) => bgGray(s)  // optional background function\n);\ntext.setText(\"Updated\");\n```\n\n### Caja\n\nContenedor con relleno y color de fondo.\n\n```typescript\nconst box = new Box(\n  1,                // paddingX\n  1,                // paddingY\n  (s) => bgGray(s)  // background function\n);\nbox.addChild(new Text(\"Content\", 0, 0));\nbox.setBgFn((s) => bgBlue(s));\n```\n\n### Recipiente\n\nAgrupa los componentes secundarios verticalmente.\n\n```typescript\nconst container = new Container();\ncontainer.addChild(component1);\ncontainer.addChild(component2);\ncontainer.removeChild(component1);\n```\n\n### Espaciador\n\nEspacio vertical vacío.\n\n```typescript\nconst spacer = new Spacer(2);  // 2 empty lines\n```\n\n### Markdown\n\nRepresenta la reducción con resaltado de sintaxis.\n\n```typescript\nconst md = new Markdown(\n  \"# Title\\n\\nSome **bold** text\",\n  1,        // paddingX\n  1,        // paddingY\n  theme     // MarkdownTheme (see below)\n);\nmd.setText(\"Updated markdown\");\n```\n\n### Imagen\n\nRenderiza imágenes en terminales compatibles (Kitty, iTerm2, Ghostty, WezTerm, Warp).\n\n```typescript\nconst image = new Image(\n  base64Data,   // base64-encoded image\n  \"image/png\",  // MIME type\n  theme,        // ImageTheme\n  { maxWidthCells: 80, maxHeightCells: 24 }\n);\n```\n\n## Entrada de teclado\n\nUtilice `matchesKey()` para la detección de claves:\n\n```typescript\nimport { matchesKey, Key } from \"@earendil-works/pi-tui\";\n\nhandleInput(data: string) {\n  if (matchesKey(data, Key.up)) {\n    this.selectedIndex--;\n  } else if (matchesKey(data, Key.enter)) {\n    this.onSelect?.(this.selectedIndex);\n  } else if (matchesKey(data, Key.escape)) {\n    this.onCancel?.();\n  } else if (matchesKey(data, Key.ctrl(\"c\"))) {\n    // Ctrl+C\n  }\n}\n```\n\n**Identificadores de clave** (use `Key.*` para autocompletar o literales de cadena):\n- Teclas básicas: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`\n- Teclas de flecha: `Key.up`, `Key.down`, `Key.left`, `Key.right`\n- Con modificadores: `Key.ctrl(\"c\")`, `Key.shift(\"tab\")`, `Key.alt(\"left\")`, `Key.ctrlShift(\"p\")`\n- El formato de cadena también funciona: `\"enter\"`, `\"ctrl+c\"`, `\"shift+tab\"`, `\"ctrl+shift+p\"`\n\n## Ancho de línea\n\n**Crítico:** Cada línea desde `render()` no debe exceder el parámetro `width`.\n\n```typescript\nimport { visibleWidth, truncateToWidth } from \"@earendil-works/pi-tui\";\n\nrender(width: number): string[] {\n  // Truncate long lines\n  return [truncateToWidth(this.text, width)];\n}\n```\n\nUtilidades:\n- `visibleWidth(str)`: obtiene el ancho de visualización (ignora los códigos ANSI)\n- `truncateToWidth(str, width, ellipsis?)` - Truncar con puntos suspensivos opcionales\n- `wrapTextWithAnsi(str, width)` - Ajuste de texto que conserva los códigos ANSI\n\n## Crear componentes personalizados\n\nEjemplo: selector interactivo\n\n```typescript\nimport {\n  matchesKey, Key,\n  truncateToWidth, visibleWidth\n} from \"@earendil-works/pi-tui\";\n\nclass MySelector {\n  private items: string[];\n  private selected = 0;\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n  \n  public onSelect?: (item: string) => void;\n  public onCancel?: () => void;\n\n  constructor(items: string[]) {\n    this.items = items;\n  }\n\n  handleInput(data: string): void {\n    if (matchesKey(data, Key.up) && this.selected > 0) {\n      this.selected--;\n      this.invalidate();\n    } else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {\n      this.selected++;\n      this.invalidate();\n    } else if (matchesKey(data, Key.enter)) {\n      this.onSelect?.(this.items[this.selected]);\n    } else if (matchesKey(data, Key.escape)) {\n      this.onCancel?.();\n    }\n  }\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n\n    this.cachedLines = this.items.map((item, i) => {\n      const prefix = i === this.selected ? \"> \" : \"  \";\n      return truncateToWidth(prefix + item, width);\n    });\n    this.cachedWidth = width;\n    return this.cachedLines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\nUso en una extensión:\n\n```typescript\npi.registerCommand(\"pick\", {\n  description: \"Pick an item\",\n  handler: async (_args, ctx) => {\n    const items = [\"Option A\", \"Option B\", \"Option C\"];\n    const selected = await ctx.ui.custom<string | null>((tui, _theme, _keybindings, done) => {\n      const selector = new MySelector(items);\n      selector.onSelect = done;\n      selector.onCancel = () => done(null);\n\n      return {\n        render: (width) => selector.render(width),\n        handleInput: (data) => {\n          selector.handleInput(data);\n          tui.requestRender();\n        },\n        invalidate: () => selector.invalidate(),\n      };\n    });\n\n    if (selected !== null) {\n      ctx.ui.notify(`Selected: ${selected}`, \"info\");\n    }\n  }\n});\n```\n\n## Tematización\n\nLos componentes aceptan objetos temáticos para diseñar.\n\n**En `renderCall`/`renderResult`**, use el parámetro `theme`:\n\n```typescript\nrenderResult(result, options, theme, context) {\n  // Use theme.fg() for foreground colors\n  return new Text(theme.fg(\"success\", \"Done!\"), 0, 0);\n  \n  // Use theme.bg() for background colors\n  const styled = theme.bg(\"toolPendingBg\", theme.fg(\"accent\", \"text\"));\n}\n```\n\n**Colores de primer plano** (`theme.fg(color, text)`):\n\n| Categoría | Bandera |\n|----------|--------|\n| General | `text`, `accent`, `muted`, `dim` |\n| Estado | `success`, `error`, `warning` |\n| Fronteras | `border`, `borderAccent`, `borderMuted` |\n| Mensajes | `userMessageText`, `customMessageText`, `customMessageLabel` |\n| Herramientas | `toolTitle`, `toolOutput` |\n| diferencias | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |\n| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |\n| Sintaxis | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |\n| Pensamiento | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` |\n| Modos | `bashMode` |\n\n**Colores de fondo** (`theme.bg(color, text)`):\n\n`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`\n\n**Para Markdown**, use `getMarkdownTheme()`:\n\n```typescript\nimport { getMarkdownTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Markdown } from \"@earendil-works/pi-tui\";\n\nrenderResult(result, options, theme, context) {\n  const mdTheme = getMarkdownTheme();\n  return new Markdown(result.details.markdown, 0, 0, mdTheme);\n}\n```\n\n**Para componentes personalizados**, defina su propia interfaz de tema:\n\n```typescript\ninterface MyTheme {\n  selected: (s: string) => string;\n  normal: (s: string) => string;\n}\n```\n\n## Registro de depuración\n\nConfigure `PI_TUI_WRITE_LOG` para capturar la secuencia ANSI sin procesar escrita en stdout.\n\n```bash\nPI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts\n```\n\n## Actuación\n\nCaché de salida renderizada cuando sea posible:\n\n```typescript\nclass CachedComponent {\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n    // ... compute lines ...\n    this.cachedWidth = width;\n    this.cachedLines = lines;\n    return lines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\nLlame a `invalidate()` cuando cambie el estado, luego use el `tui.requestRender()` inyectado para activar la nueva renderización.\n\n## Invalidación y cambios de tema\n\nCuando el tema cambia, TUI llama a `invalidate()` a todos los componentes para borrar sus cachés. Los componentes deben implementar correctamente `invalidate()` para garantizar que los cambios del tema surtan efecto.\n\n### El problema\n\nSi un componente convierte previamente los colores del tema en cadenas (a través de `theme.fg()`, `theme.bg()`, etc.) y los almacena en caché, las cadenas almacenadas en caché contienen códigos de escape ANSI del tema anterior. Simplemente borrar el caché de renderizado no es suficiente si el componente almacena el contenido temático por separado.\n\n**Enfoque incorrecto** (los colores del tema no se actualizarán):\n\n```typescript\nclass BadComponent extends Container {\n  private content: Text;\n\n  constructor(message: string, theme: Theme) {\n    super();\n    // Pre-baked theme colors stored in Text component\n    this.content = new Text(theme.fg(\"accent\", message), 1, 0);\n    this.addChild(this.content);\n  }\n  // No invalidate override - parent's invalidate only clears\n  // child render caches, not the pre-baked content\n}\n```\n\n### La solución\n\nLos componentes que crean contenido con colores de tema deben reconstruir ese contenido cuando se llama a `invalidate()`:\n\n```typescript\nclass GoodComponent extends Container {\n  private message: string;\n  private content: Text;\n\n  constructor(message: string) {\n    super();\n    this.message = message;\n    this.content = new Text(\"\", 1, 0);\n    this.addChild(this.content);\n    this.updateDisplay();\n  }\n\n  private updateDisplay(): void {\n    // Rebuild content with current theme\n    this.content.setText(theme.fg(\"accent\", this.message));\n  }\n\n  override invalidate(): void {\n    super.invalidate();  // Clear child caches\n    this.updateDisplay(); // Rebuild with new theme\n  }\n}\n```\n\n### Patrón: reconstruir al invalidar\n\nPara componentes con contenido complejo:\n\n```typescript\nclass ComplexComponent extends Container {\n  private data: SomeData;\n\n  constructor(data: SomeData) {\n    super();\n    this.data = data;\n    this.rebuild();\n  }\n\n  private rebuild(): void {\n    this.clear();  // Remove all children\n\n    // Build UI with current theme\n    this.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Title\")), 1, 0));\n    this.addChild(new Spacer(1));\n\n    for (const item of this.data.items) {\n      const color = item.active ? \"success\" : \"muted\";\n      this.addChild(new Text(theme.fg(color, item.label), 1, 0));\n    }\n  }\n\n  override invalidate(): void {\n    super.invalidate();\n    this.rebuild();\n  }\n}\n```\n\n### Cuando esto importa\n\nEste patrón es necesario cuando:\n\n1. **Colores del tema previo al horneado**: uso de `theme.fg()` o `theme.bg()` para crear cadenas con estilo almacenadas en componentes secundarios\n2. **Resaltado de sintaxis**: uso de `highlightCode()`, que aplica colores de sintaxis basados ​​en temas\n3. **Diseños complejos** - Creación de árboles de componentes secundarios que incorporan colores de temas\n\nEste patrón NO es necesario cuando:\n\n1. **Usar devoluciones de llamadas de temas** - Pasar funciones como `(text) => theme.fg(\"accent\", text)` que se llaman durante el renderizado\n2. **Contenedores simples**: simplemente agrupa otros componentes sin agregar contenido temático\n3. **Renderizado sin estado**: Computación de salida temática nueva en cada llamada `render()` (sin almacenamiento en caché)\n\n## Patrones comunes\n\nEstos patrones cubren las necesidades de interfaz de usuario más comunes en las extensiones. **Copia estos patrones en lugar de construir desde cero.**\n\n### Patrón 1: Diálogo de selección (SelectList)\n\nPara permitir a los usuarios elegir de una lista de opciones. Utilice `SelectList` de `@earendil-works/pi-tui` con `DynamicBorder` para enmarcar.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { DynamicBorder } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SelectItem, SelectList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"pick\", {\n  handler: async (_args, ctx) => {\n    const items: SelectItem[] = [\n      { value: \"opt1\", label: \"Option 1\", description: \"First option\" },\n      { value: \"opt2\", label: \"Option 2\", description: \"Second option\" },\n      { value: \"opt3\", label: \"Option 3\" },  // description is optional\n    ];\n\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const container = new Container();\n\n      // Top border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      // Title\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Pick an Option\")), 1, 0));\n\n      // SelectList with theme\n      const selectList = new SelectList(items, Math.min(items.length, 10), {\n        selectedPrefix: (t) => theme.fg(\"accent\", t),\n        selectedText: (t) => theme.fg(\"accent\", t),\n        description: (t) => theme.fg(\"muted\", t),\n        scrollInfo: (t) => theme.fg(\"dim\", t),\n        noMatch: (t) => theme.fg(\"warning\", t),\n      });\n      selectList.onSelect = (item) => done(item.value);\n      selectList.onCancel = () => done(null);\n      container.addChild(selectList);\n\n      // Help text\n      container.addChild(new Text(theme.fg(\"dim\", \"↑↓ navigate • enter select • esc cancel\"), 1, 0));\n\n      // Bottom border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },\n      };\n    });\n\n    if (result) {\n      ctx.ui.notify(`Selected: ${result}`, \"info\");\n    }\n  },\n});\n```\n\n**Ejemplos:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)\n\n### Patrón 2: operación asíncrona con cancelación (BorderedLoader)\n\nPara operaciones que toman tiempo y deberían ser cancelables. `BorderedLoader` muestra una ruleta y maneja el escape para cancelar.\n\n```typescript\nimport { BorderedLoader } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"fetch\", {\n  handler: async (_args, ctx) => {\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const loader = new BorderedLoader(tui, theme, \"Fetching data...\");\n      loader.onAbort = () => done(null);\n\n      // Do async work\n      fetchData(loader.signal)\n        .then((data) => done(data))\n        .catch(() => done(null));\n\n      return loader;\n    });\n\n    if (result === null) {\n      ctx.ui.notify(\"Cancelled\", \"info\");\n    } else {\n      ctx.ui.setEditorText(result);\n    }\n  },\n});\n```\n\n**Ejemplos:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)\n\n### Patrón 3: Configuración/Alternancia (Lista de configuración)\n\nPara alternar múltiples configuraciones. Utilice `SettingsList` de `@earendil-works/pi-tui` con `getSettingsListTheme()`.\n\n```typescript\nimport { getSettingsListTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SettingItem, SettingsList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"settings\", {\n  handler: async (_args, ctx) => {\n    const items: SettingItem[] = [\n      { id: \"verbose\", label: \"Verbose mode\", currentValue: \"off\", values: [\"on\", \"off\"] },\n      { id: \"color\", label: \"Color output\", currentValue: \"on\", values: [\"on\", \"off\"] },\n    ];\n\n    await ctx.ui.custom((_tui, theme, _kb, done) => {\n      const container = new Container();\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Settings\")), 1, 1));\n\n      const settingsList = new SettingsList(\n        items,\n        Math.min(items.length + 2, 15),\n        getSettingsListTheme(),\n        (id, newValue) => {\n          // Handle value change\n          ctx.ui.notify(`${id} = ${newValue}`, \"info\");\n        },\n        () => done(undefined),  // On close\n        { enableSearch: true }, // Optional: enable fuzzy search by label\n      );\n      container.addChild(settingsList);\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => settingsList.handleInput?.(data),\n      };\n    });\n  },\n});\n```\n\n**Ejemplos:** [tools.ts](../examples/extensions/tools.ts)\n\n### Patrón 4: Indicador de estado persistente\n\nMuestra el estado en el pie de página que persiste en todos los renderizados. Bueno para indicadores de modo.\n\n```typescript\n// Set status (shown in footer)\nctx.ui.setStatus(\"my-ext\", ctx.ui.theme.fg(\"accent\", \"● active\"));\n\n// Clear status\nctx.ui.setStatus(\"my-ext\", undefined);\n```\n\n**Ejemplos:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)\n\n### Patrón 4b: Personalización del indicador de trabajo\n\nPersonalice el indicador de trabajo en línea que se muestra mientras pi transmite una respuesta.\n\n```typescript\n// Static indicator\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });\n\n// Custom animated indicator\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\n\n// Hide the indicator entirely\nctx.ui.setWorkingIndicator({ frames: [] });\n\n// Restore pi's default spinner\nctx.ui.setWorkingIndicator();\n```\n\nEsto sólo afecta al indicador de funcionamiento normal de la transmisión. Los cargadores de compactación y reintento mantienen su estilo incorporado. Los marcos personalizados se representan palabra por palabra, por lo que las extensiones deben agregar sus propios colores cuando sea necesario.\n\n**Ejemplos:** [working-indicator.ts](../examples/extensions/working-indicator.ts)\n\n### Patrón 5: Editor de widgets arriba/abajo\n\nMuestra contenido persistente encima o debajo del editor de entrada. Bueno para listas de tareas pendientes, progreso.\n\n```typescript\n// Simple string array (above editor by default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n\n// Render below the editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\n\n// Or with theme\nctx.ui.setWidget(\"my-widget\", (_tui, theme) => {\n  const lines = items.map((item, i) =>\n    item.done\n      ? theme.fg(\"success\", \"✓ \") + theme.fg(\"muted\", item.text)\n      : theme.fg(\"dim\", \"○ \") + item.text\n  );\n  return {\n    render: () => lines,\n    invalidate: () => {},\n  };\n});\n\n// Clear\nctx.ui.setWidget(\"my-widget\", undefined);\n```\n\n**Ejemplos:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)\n\n### Patrón 6: pie de página personalizado\n\nReemplace el pie de página. `footerData` expone datos a los que las extensiones no pueden acceder de otro modo.\n\n```typescript\nctx.ui.setFooter((tui, theme, footerData) => ({\n  invalidate() {},\n  render(width: number): string[] {\n    // footerData.getGitBranch(): string | null\n    // footerData.getExtensionStatuses(): ReadonlyMap<string, string>\n    return [`${ctx.model?.id} (${footerData.getGitBranch() || \"no git\"})`];\n  },\n  dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive\n}));\n\nctx.ui.setFooter(undefined); // restore default\n```\n\nEstadísticas de tokens disponibles a través de `ctx.sessionManager.getBranch()` y `ctx.model`.\n\n**Ejemplos:** [custom-footer.ts](../examples/extensions/custom-footer.ts)\n\n### Patrón 7: Editor personalizado (modo vim, etc.)\n\nReemplace el editor de entrada principal con una implementación personalizada. Útil para edición modal (vim), diferentes combinaciones de teclas (emacs) o manejo de entrada especializado.\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey, truncateToWidth } from \"@earendil-works/pi-tui\";\n\ntype Mode = \"normal\" | \"insert\";\n\nclass VimEditor extends CustomEditor {\n  private mode: Mode = \"insert\";\n\n  handleInput(data: string): void {\n    // Escape: switch to normal mode, or pass through for app handling\n    if (matchesKey(data, \"escape\")) {\n      if (this.mode === \"insert\") {\n        this.mode = \"normal\";\n        return;\n      }\n      // In normal mode, escape aborts agent (handled by CustomEditor)\n      super.handleInput(data);\n      return;\n    }\n\n    // Insert mode: pass everything to CustomEditor\n    if (this.mode === \"insert\") {\n      super.handleInput(data);\n      return;\n    }\n\n    // Normal mode: vim-style navigation\n    switch (data) {\n      case \"i\": this.mode = \"insert\"; return;\n      case \"h\": super.handleInput(\"\\x1b[D\"); return; // Left\n      case \"j\": super.handleInput(\"\\x1b[B\"); return; // Down\n      case \"k\": super.handleInput(\"\\x1b[A\"); return; // Up\n      case \"l\": super.handleInput(\"\\x1b[C\"); return; // Right\n    }\n    // Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars\n    if (data.length === 1 && data.charCodeAt(0) >= 32) return;\n    super.handleInput(data);\n  }\n\n  render(width: number): string[] {\n    const lines = super.render(width);\n    // Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)\n    if (lines.length > 0) {\n      const label = this.mode === \"normal\" ? \" NORMAL \" : \" INSERT \";\n      const lastLine = lines[lines.length - 1]!;\n      // Pass \"\" as ellipsis to avoid adding \"...\" when truncating\n      lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, \"\") + label;\n    }\n    return lines;\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    // Factory receives the TUI, theme, and keybindings from the app\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**Puntos clave:**\n\n- **Extienda `CustomEditor`** (no la base `Editor`) para obtener combinaciones de teclas de la aplicación (escape para cancelar, Ctrl+d para salir, cambio de modelo, etc.)\n- **Llame al `super.handleInput(data)`** para llaves que no maneja\n- **Patrón de fábrica**: `setEditorComponent` recibe una función de fábrica que obtiene `tui`, `theme` y `keybindings`\n- **Pase `undefined`** para restaurar el editor predeterminado: `ctx.ui.setEditorComponent(undefined)`\n\n**Ejemplos:** [modal-editor.ts](../examples/extensions/modal-editor.ts)\n\n## Reglas clave\n\n1. **Utilice siempre el tema de la devolución de llamada** - No importe el tema directamente. Utilice `theme` de la devolución de llamada `ctx.ui.custom((tui, theme, keybindings, done) =>...)`.\n\n2. **Escriba siempre el parámetro de color DynamicBorder** - Escriba `(s: string) => theme.fg(\"accent\", s)`, no `(s) => theme.fg(\"accent\", s)`.\n\n3. **Llame a tui.requestRender() después de cambios de estado** - En `handleInput`, llame a `tui.requestRender()` después de actualizar el estado.\n\n4. **Devolver el objeto de tres métodos** - Los componentes personalizados necesitan `{ render, invalidate, handleInput }`.\n\n5. **Utilice componentes existentes**: `SelectList`, `SettingsList`, `BorderedLoader` cubren el 90 % de los casos. No los reconstruyas.\n\n## Ejemplos\n\n- **UI de selección**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList con marco DynamicBorder\n- **Asíncrono con cancelación**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader para llamadas LLM\n- **Configuración alterna**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - Lista de configuraciones para habilitar/deshabilitar herramientas\n- **Indicadores de estado**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus y setWidget\n- **Indicador de trabajo**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator\n- **Pie de página personalizado**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter con estadísticas\n- **Editor personalizado**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Edición modal similar a Vim\n- **Juego de serpiente**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Juego completo con entrada de teclado, bucle de juego\n- **Herramienta de renderizado personalizada**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall y renderResult","sourceFile":"tui.md"},"usage":{"title":"Usando Pi","markdown":"Esta página recopila detalles de uso diario que no caben en la página de inicio rápido.\n\n## Modo interactivo\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Interactive Mode\" width=\"600\"></p>\n\nLa interfaz tiene cuatro áreas principales:\n\n- **Encabezado de inicio**: atajos, cargados context files, prompt templates, habilidades y extensiones\n- **Mensajes**: mensajes de usuario, respuestas del asistente, llamadas de herramientas, resultados de herramientas, notificaciones, errores y interfaz de usuario de extensión\n- **Editor** - donde escribes; El color del borde indica el nivel de pensamiento actual.\n- **Pie de página**: directorio de trabajo, nombre de la sesión, uso de token/caché, costo, uso de contexto y modelo actual. Los totales incluyen las respuestas del asistente, el uso informado por las herramientas y la generación de resúmenes.\n\nEl editor se puede reemplazar temporalmente por una interfaz de usuario integrada como `/settings` o por una interfaz de usuario de extensión personalizada.\n\n### Funciones del editor\n\n| Característica | Cómo |\n|---------|-----|\n| Referencia de archivo | Escriba `@` para realizar una búsqueda aproximada de archivos de proyecto |\n| Finalización del camino | Presione Tab para completar rutas |\n| Entrada multilínea | Mayús+Entrar o Ctrl+Entrar en la Terminal de Windows |\n| Copiar respuesta | Ctrl+X copia el último mensaje del asistente; en `/tree`, copia el mensaje seleccionado |\n| Imágenes | Pegue con Ctrl+V, Alt+V en Windows o arrastre al terminal |\n| Comando de shell | `!command` ejecuta y envía salida al modelo |\n| Comando de shell oculto | `!!command` se ejecuta sin enviar salida al modelo |\n| editor externo | Ctrl+G abre `externalEditor`, `$VISUAL`, `$EDITOR`, el Bloc de notas en Windows o `nano` en otro lugar |\n\nConsulte [Keybindings](keybindings.md) para conocer todos los atajos y personalizaciones.\n\n## Comandos de barra diagonal\n\nEscriba `/` en el editor para abrir la finalización de comandos. Extensions puede registrar comandos personalizados, las habilidades están disponibles como `/skill:name` y prompt templates se expanden a través de `/templatename`.\n\n| Dominio | Descripción |\n|---------|-------------|\n| `/login`, `/logout` | Administrar credenciales de clave OAuth o API |\n| [`/llama`](llama-cpp.md) | Descargar, cargar y descargar modelos de enrutador llama.cpp |\n| `/model` | Cambiar modelos |\n| `/scoped-models` | Activar/desactivar modelos para el ciclo Ctrl+P |\n| `/settings` | Nivel de pensamiento, tema, entrega de mensajes, transporte. |\n| `/resume` | Pick de sesiones anteriores |\n| `/new` | Iniciar una nueva sesión |\n| `/name <name>` | Establecer el nombre para mostrar de la sesión |\n| `/session` | Mostrar archivo de sesión, ID, mensajes, tokens y costo |\n| `/tree` | Salta a cualquier punto de la sesión y continúa desde allí. |\n| `/trust` | Guarde la decisión de confianza del proyecto para sesiones futuras |\n| `/fork` | Crear una nueva sesión a partir de un mensaje de usuario anterior |\n| `/clone` | Duplicar la rama activa actual en una nueva sesión |\n| `/compact [prompt]` | Contexto compacto manualmente, opcionalmente con instrucciones personalizadas |\n| `/copy` | Copiar el último mensaje del asistente al portapapeles |\n| `/export [file]` | Exportar sesión a HTML o JSONL |\n| `/import <file>` | Importar y reanudar una sesión desde un archivo JSONL |\n| `/share` | Subir como GitHub privado con enlace HTML para compartir |\n| `/reload` | Vuelva a cargar combinaciones de teclas, extensiones, habilidades, indicaciones, temas y context files |\n| `/hotkeys` | Mostrar todos los atajos de teclado |\n| `/changelog` | Mostrar historial de versiones |\n| `/quit` | dejar pi |\n\n## Cola de mensajes\n\nPuede enviar mensajes mientras el agente aún está trabajando:\n\n- **Introducir** pone en cola un mensaje de dirección, entregado después de que el turno actual del asistente termina de ejecutar sus llamadas de herramienta.\n- **Alt+Intro** pone en cola un mensaje de seguimiento, que se entrega después de que el agente finaliza todo el trabajo.\n- **Escape** cancela y restaura los mensajes en cola en el editor.\n- **Alt+Arriba** recupera los mensajes en cola y los envía al editor.\n\nEn Windows Terminal, Alt+Enter está en pantalla completa de forma predeterminada. Vuelva a asignarlo como se describe en [Terminal setup](terminal-setup.md) si desea que pi reciba el acceso directo.\n\nConfigura la entrega en [Settings](settings.md) con `steeringMode` y `followUpMode`.\n\n## Sesiones\n\nLas sesiones se guardan automáticamente en `~/.pi/agent/sessions/`, organizadas por directorio de trabajo.\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select a session\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or session ID\npi --fork <path|id>    # Fork a session into a new session file\n```\n\nComandos de sesión útiles:\n\n- `/session` muestra el archivo de sesión actual y el ID.\n- `/tree` navega por el archivo session tree y puede resumir las ramas abandonadas.\n- `/fork` crea una nueva sesión a partir de un mensaje de usuario anterior.\n- `/clone` duplica la rama activa actual en un nuevo archivo de sesión.\n- `/compact` resume mensajes antiguos en contexto libre.\n\nConsulte [Sessions](sessions.md) y [Compaction](compaction.md) para obtener más detalles.\n\n## Archivos de contexto\n\nPi carga `AGENTS.md` o `CLAUDE.md` al inicio desde:\n\n- `~/.pi/agent/AGENTS.md` para instrucciones globales\n- directorios principales, subiendo desde el directorio de trabajo actual\n- el directorio actual\n\nSi un directorio contiene `AGENTS.override.md`, Pi lo carga en lugar de `AGENTS.md` o `CLAUDE.md` de ese directorio. Los archivos de contexto de otros directorios todavía se superponen normalmente.\n\nUtilice context files para convenciones, comandos, reglas de seguridad y preferencias del proyecto. Deshabilite la carga con `--no-context-files` o `-nc`.\n\n### Archivos de aviso del sistema\n\nReemplace el mensaje predeterminado del sistema con:\n\n- `.pi/SYSTEM.md` para un proyecto\n- `~/.pi/agent/SYSTEM.md` globalmente\n\nAgregue al mensaje predeterminado sin reemplazarlo con `APPEND_SYSTEM.md` en ninguna ubicación.\n\n### Confianza del proyecto\n\nEn el inicio interactivo, pi pregunta antes de confiar en una carpeta de proyecto que contiene configuraciones, recursos o proyecto local del proyecto `.agents/skills` y no tiene ninguna decisión guardada para la carpeta o una carpeta principal en `~/.pi/agent/trust.json`. Confiar en un proyecto permite a pi cargar recursos `.pi/settings.json` y `.pi`, instalar paquetes de proyecto faltantes y ejecutar extensiones de proyecto.\n\nAntes de la decisión de confianza, pi carga solo context files, extensiones de usuario/globales y extensiones CLI `-e` para que puedan manejar el evento `project_trust`. Las extensiones locales del proyecto, las extensiones administradas por paquetes del proyecto y la configuración del proyecto se cargan solo después de que el proyecto sea confiable. Esta división también se aplica cuando se cambia a una sesión desde un cwd diferente cuya confianza no se ha resuelto en el proceso actual.\n\nLos modos no interactivos (`-p`, `--mode json` y `--mode rpc`) no muestran un mensaje de confianza. Sin una decisión de confianza guardada aplicable, usan `defaultProjectTrust` de la configuración global: `ask` (predeterminado) y `never` ignoran esos recursos del proyecto, mientras que `always` confía en ellos. Pase `--approve`/`-a` o `--no-approve`/`-na` para anular la confianza del proyecto durante una ejecución.\n\nSi no se aplica ninguna extensión o decisión guardada, `defaultProjectTrust` controla el comportamiento de reserva. Configúrelo en `\"ask\"`, `\"always\"` o `\"never\"` en `~/.pi/agent/settings.json`, o cámbielo con `/settings`.\n\nLos comandos `pi config` y del paquete usan el mismo flujo de confianza del proyecto, excepto que `pi update` nunca lo solicita. Pase `--approve` para confiar en la configuración local del proyecto para un comando o `--no-approve` para ignorarlos.\n\nUtilice `/trust` en modo interactivo para guardar una decisión de confianza del proyecto para sesiones futuras, incluida la confianza para la carpeta principal inmediata. Escribe solo `~/.pi/agent/trust.json`; la sesión actual no se recarga, así que reinicie pi para que los cambios surtan efecto.\n\n\n## Exportar y compartir sesiones\n\nUtilice `/export [file]` para escribir una sesión en HTML.\n\nUtilice `/share` para cargar una esencia GitHub privada con un enlace HTML para compartir.\n\nSi utiliza pi para trabajos de código abierto y desea publicar sesiones para investigación de modelos, indicaciones, herramientas y evaluación, consulte [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Publica sesiones en Hugging Face conjuntos de datos.\n\n## CLI Referencia\n\n```bash\npi [options] [@files...] [messages...]\n```\n\n### Comandos de paquete\n\n```bash\npi install <source> [-l]     # Install package, -l for project-local\npi remove <source> [-l]      # Remove package\npi uninstall <source> [-l]   # Alias for remove\npi update [source|self|pi]   # Update pi only, or one package source\npi update --all              # Update pi and packages; reconcile pinned git refs\npi update --extensions       # Update packages only; reconcile pinned git refs\npi update --models           # Refresh model catalogs only\npi update --self             # Update pi only\npi update --extension <src>  # Update one package\npi list                      # List installed packages\npi config                    # Enable/disable package resources\n```\n\nEstos comandos administran paquetes pi y `pi update` pueden actualizar la instalación de pi CLI. Para desinstalar pi, consulte [Quickstart](quickstart.md#uninstall). `pi config` y los comandos del paquete del proyecto aceptan `--approve`/`--no-approve` para confiar o ignorar la configuración local del proyecto para un comando. `pi update` nunca solicita confianza en el proyecto.\n\nConsulte [Pi Packages](packages.md) para conocer los orígenes de los paquetes y las notas de seguridad.\n\n### Modos\n\n| Bandera | Descripción |\n|------|-------------|\n| por defecto | Modo interactivo |\n| `-p`, `--print` | Imprimir respuesta y salir |\n| `--mode json` | Muestra todos los eventos como JSON líneas; ver [JSON mode](json.md) |\n| `--mode rpc` | RPC modo terminado stdin/stdout; ver [RPC mode](rpc.md) |\n| `--export <in> [out]` | Exportar una sesión a HTML |\n\nEn el modo de impresión, pi también lee canalizado stdin y lo combina con el mensaje inicial:\n\n```bash\ncat README.md | pi -p \"Summarize this text\"\n```\n\n### Opciones de modelo\n\n| Opción | Descripción |\n|--------|-------------|\n| `--provider <name>` | Proveedor, como `anthropic`, `openai` o `google` |\n| `--model <pattern>` | Patrón de modelo o identificación; admite `provider/id` y opcional `:<thinking>` |\n| `--api-key <key>` | API key, anulando variables de entorno |\n| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` |\n| `--models <patterns>` | Patrones separados por comas para el ciclo Ctrl+P |\n| `--list-models [search]` | Listar modelos disponibles |\n\n### Opciones de sesión\n\n| Opción | Descripción |\n|--------|-------------|\n| `-c`, `--continue` | Continuar la sesión más reciente |\n| `-r`, `--resume` | Navega y selecciona una sesión |\n| `--sesión <ruta\\ | identificación>` | Utilice un archivo de sesión específico o un UUID parcial |\n| `--fork <ruta\\ | identificación>` | Bifurca un archivo de sesión o un UUID parcial en una nueva sesión |\n| `--session-dir <dir>` | Directorio de almacenamiento de sesión personalizado |\n| `--no-session` | Modo efímero; no guardar |\n| `--name <name>`, `-n <name>` | Establecer el nombre para mostrar de la sesión al inicio |\n\n### Opciones de herramientas\n\n| Opción | Descripción |\n|--------|-------------|\n| `--tools <list>`, `-t <list>` | Herramientas integradas, de extensión y personalizadas específicas de la lista blanca |\n| `--exclude-tools <list>`, `-xt <list>` | Deshabilite herramientas específicas integradas, de extensión y personalizadas |\n| `--no-builtin-tools`, `-nbt` | Deshabilite las herramientas integradas pero mantenga habilitadas las herramientas de extensión/personalizadas |\n| `--no-tools`, `-nt` | Deshabilitar todas las herramientas |\n\nHerramientas integradas: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`.\n\n### Opciones de recursos\n\n| Opción | Descripción |\n|--------|-------------|\n| `-e`, `--extension <source>` | Cargue una extensión desde la ruta, npm o git; repetible |\n| `--no-extensions` | Deshabilitar el descubrimiento de extensiones |\n| `--skill <path>` | Cargar una habilidad; repetible |\n| `--no-skills` | Deshabilitar el descubrimiento de habilidades |\n| `--prompt-template <path>` | Cargue una plantilla de aviso; repetible |\n| `--no-prompt-templates` | Deshabilitar el descubrimiento de plantillas de mensajes |\n| `--theme <path>` | Cargar un tema; repetible |\n| `--no-themes` | Deshabilitar el descubrimiento de temas |\n| `--no-context-files`, `-nc` | Deshabilitar el descubrimiento `AGENTS.md` y `CLAUDE.md` |\n\nCombine `--no-*` con indicadores explícitos para cargar exactamente lo que necesita, ignorando la configuración. Ejemplo:\n\n```bash\npi --no-extensions -e ./my-extension.ts\n```\n\n### Otras opciones\n\n| Opción | Descripción |\n|--------|-------------|\n| `--system-prompt <text>` | Reemplazar mensaje predeterminado; context files y las habilidades aún están agregadas |\n| `--append-system-prompt <text>` | Agregar al mensaje del sistema |\n| `--tui-mode <mode>` | Modo TUI: `regular` (predeterminado) o experimental `fullscreen` |\n| `--verbose` | Forzar inicio detallado |\n| `-a`, `--approve` | Confíe en los archivos locales del proyecto para esta ejecución |\n| `-na`, `--no-approve` | Ignorar archivos locales del proyecto para esta ejecución |\n| `-h`, `--help` | Mostrar ayuda |\n| `-v`, `--version` | Mostrar versión |\n\nEn el modo `fullscreen`, la transcripción se desplaza dentro de la ventana gráfica del terminal mientras los mensajes en cola, el estado de trabajo, los widgets de extensión, el editor y el pie de página permanecen fijos en la parte inferior. La entrada del mouse/trackpad desplaza la región debajo del puntero; Las acciones de la ventana gráfica del teclado siempre permanecen disponibles. Las imágenes en línea funcionan en terminales que admiten el protocolo de gráficos Kitty, incluidos Kitty y Ghostty. En iTerm2, se representan como marcadores de posición de texto porque su protocolo de imágenes en línea no puede eliminar ni recortar ubicaciones durante el desplazamiento propiedad de la aplicación. En el modo `regular`, pi usa la pantalla principal y el desplazamiento hacia atrás propiedad del terminal, y las imágenes en línea de iTerm2 continúan representándose normalmente.\n\nConfigure **TUI modo** en `/settings` para cambiar entre `regular` y `fullscreen` inmediatamente y elija el modo predeterminado para sesiones futuras. **Salida de salida en pantalla completa** controla si al salir de la pantalla completa se imprime la transcripción final o se restaura la pantalla anterior e imprime solo la sugerencia de reanudación de la sesión.\n\n### Argumentos de archivo\n\nPrefije los archivos con `@` para incluirlos en el mensaje:\n\n```bash\npi @prompt.md \"Answer this\"\npi -p @screenshot.png \"What's in this image?\"\npi @code.ts @test.ts \"Review these files\"\n```\n\n### Ejemplos\n\n```bash\n# Interactive with initial prompt\npi \"List all .ts files in src/\"\n\n# Non-interactive\npi -p \"Summarize this codebase\"\n\n# Non-interactive with piped stdin\ncat README.md | pi -p \"Summarize this text\"\n\n# Named one-shot session\npi --name \"release audit\" -p \"Audit this repository\"\n\n# Different model\npi --provider openai --model gpt-4o \"Help me refactor\"\n\n# Model with provider prefix\npi --model openai/gpt-4o \"Help me refactor\"\n\n# Model with thinking level shorthand\npi --model sonnet:high \"Solve this complex problem\"\n\n# Limit model cycling\npi --models \"claude-*,gpt-4o\"\n\n# Read-only mode\npi --tools read,grep,find,ls -p \"Review the code\"\n\n# Disable one extension or built-in tool while keeping the rest available\npi --exclude-tools ask_question\n```\n\n## Principios de diseño\n\nPi mantiene el núcleo pequeño e introduce el comportamiento específico del flujo de trabajo en extensiones, habilidades, prompt templates y paquetes.\n\nIntencionalmente no incluye MCP integrado, subagentes, ventanas emergentes de permiso, modo de plan, tareas pendientes o fondo bash. Puede crear o instalar esos flujos de trabajo como extensiones o paquetes, o utilizar herramientas externas como contenedores y tmux.\n\nPara conocer el fundamento completo, lea el [blog post](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/).","sourceFile":"usage.md"},"windows":{"title":"Configuración de Windows","markdown":"Pi requiere un shell bash en Windows. Ubicaciones marcadas (en orden):\n\n1. Ruta personalizada desde `~/.pi/agent/settings.json`\n2. Git Golpe (`C:\\Program Files\\Git\\bin\\bash.exe`)\n3. `bash.exe` en RUTA (Cygwin, MSYS2, WSL)\n\nPara la mayoría de los usuarios, [Git for Windows](https://git-scm.com/download/win) es suficiente.\n\n## Ruta de shell personalizada\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```","sourceFile":"windows.md"}}},"navigation":{"es":[{"title":"Comienza aquí","items":[{"title":"Pi Documentación","path":"/docs/latest","slug":"index"},{"title":"Inicio rápido","path":"/docs/latest/quickstart","slug":"quickstart"},{"title":"Usando Pi","path":"/docs/latest/usage","slug":"usage"},{"title":"Providers","path":"/docs/latest/providers","slug":"providers"},{"title":"Seguridad","path":"/docs/latest/security","slug":"security"},{"title":"Contenedorización","path":"/docs/latest/containerization","slug":"containerization"},{"title":"Ajustes","path":"/docs/latest/settings","slug":"settings"},{"title":"Combinaciones de teclas","path":"/docs/latest/keybindings","slug":"keybindings"},{"title":"Sesiones","path":"/docs/latest/sessions","slug":"sessions"},{"title":"Compactación y resumen de ramas","path":"/docs/latest/compaction","slug":"compaction"}]},{"title":"Personalización","items":[{"title":"Extensions","path":"/docs/latest/extensions","slug":"extensions"},{"title":"Skills","path":"/docs/latest/skills","slug":"skills"},{"title":"Plantillas de mensajes","path":"/docs/latest/prompt-templates","slug":"prompt-templates"},{"title":"Temas","path":"/docs/latest/themes","slug":"themes"},{"title":"Pi Packages","path":"/docs/latest/packages","slug":"packages"},{"title":"Personalizado Models","path":"/docs/latest/models","slug":"models"},{"title":"Personalizado Providers","path":"/docs/latest/custom-provider","slug":"custom-provider"}]},{"title":"Referencia","items":[{"title":"Formato de archivo de sesión","path":"/docs/latest/session-format","slug":"session-format"}]},{"title":"Uso programático","items":[{"title":"SDK","path":"/docs/latest/sdk","slug":"sdk"},{"title":"RPC Modo","path":"/docs/latest/rpc","slug":"rpc"},{"title":"JSON Modo de transmisión de eventos","path":"/docs/latest/json","slug":"json"},{"title":"TUI Componentes","path":"/docs/latest/tui","slug":"tui"}]},{"title":"Configuración de plataforma","items":[{"title":"Configuración de Windows","path":"/docs/latest/windows","slug":"windows"},{"title":"Termux (Android) Configuración","path":"/docs/latest/termux","slug":"termux"},{"title":"tmux Configuración","path":"/docs/latest/tmux","slug":"tmux"},{"title":"Configuración de terminales","path":"/docs/latest/terminal-setup","slug":"terminal-setup"},{"title":"Alias ​​de Shell","path":"/docs/latest/shell-aliases","slug":"shell-aliases"}]},{"title":"Desarrollo","items":[{"title":"Desarrollo","path":"/docs/latest/development","slug":"development"}]}]}}
