Extensions
pi pode criar extensões. Peça para criar um para o seu caso de uso.
Extensions são módulos TypeScript que estendem o comportamento do pi. Eles podem assinar eventos de ciclo de vida, registrar ferramentas personalizadas que podem ser chamadas pelo LLM, adicionar comandos e muito mais.
Posicionamento para /reload: Coloque extensões em
~/.pi/agent/extensions/(global) ou.pi/extensions/(projeto local) para descoberta automática. Usepi -e./path.tsapenas para testes rápidos. Extensions em locais descobertos automaticamente pode ser recarregado a quente com/reload.
Principais capacidades:
- Ferramentas personalizadas - Registre ferramentas que o LLM pode chamar via
pi.registerTool() - Interceptação de eventos - Bloqueie ou modifique chamadas de ferramentas, injete contexto, personalize compactação
- Interação do usuário - Avisar os usuários via
ctx.ui(selecionar, confirmar, inserir, notificar) - Componentes de UI personalizados - Componentes TUI completos com entrada de teclado via
ctx.ui.custom()para interações complexas - Comandos personalizados - Registre comandos como
/mycommandviapi.registerCommand() - Persistência de sessão - Armazena estado que sobrevive a reinicializações via
pi.appendEntry() - Renderização personalizada - Controle como as chamadas/resultados e mensagens da ferramenta aparecem em TUI
Exemplos de casos de uso:
- Portas de permissão (confirme antes de
rm -rf,sudo, etc.) - Git checkpoint (esconderijo em cada turno, restaurar na filial)
- Proteção de caminho (bloqueia gravações em
.env,node_modules/) - Compactação personalizada (resuma a conversa do seu jeito)
- Resumos de conversas (veja o exemplo
summarize.ts) - Ferramentas interativas (perguntas, assistentes, caixas de diálogo personalizadas)
- Ferramentas com estado (listas de tarefas, pools de conexões)
- Integrações externas (observadores de arquivos, webhooks, gatilhos de CI)
- Jogos enquanto você espera (veja o exemplo
snake.ts)
Veja examples/extensions/ para implementações funcionais.
Índice
- Quick Start
- Extension Locations
- Available Imports
- Writing an Extension
- Events
- ExtensionContext
- ExtensionCommandContext
- ExtensionAPI Methods
- State Management
- Custom Tools
- Custom UI
- Error Handling
- Mode Behavior
- Examples Reference
Início rápido
Crie ~/.pi/agent/extensions/my-extension.ts:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// React to events
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Extension loaded!", "info");
});
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
if (!ok) return { block: true, reason: "Blocked by user" };
}
});
// Register a custom tool
pi.registerTool({
name: "greet",
label: "Greet",
description: "Greet someone by name",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: {},
};
},
});
// Register a command
pi.registerCommand("hello", {
description: "Say hello",
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
},
});
}Teste com sinalizador --extension (ou -e):
pi -e ./my-extension.tsLocais de extensão
Segurança: Extensions executa com todas as permissões do sistema e pode executar código arbitrário. Instale apenas de fontes em que você confia.
Extensions são descobertos automaticamente em locais confiáveis. As entradas .pi/extensions locais do projeto são carregadas somente depois que o projeto é confiável.
| Localização | Escopo |
|---|---|
~/.pi/agent/extensions/*.ts |
Global (todos os projetos) |
~/.pi/agent/extensions/*/index.ts |
Global (subdiretório) |
.pi/extensions/*.ts |
Local do projeto |
.pi/extensions/*/index.ts |
Local do projeto (subdiretório) |
Caminhos adicionais via settings.json:
{
"packages": [
"npm:@foo/bar@1.0.0",
"git:github.com/user/repo@v1"
],
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension/dir"
]
}Para compartilhar extensões via npm ou git como pacotes pi, veja packages.md.
Importações disponíveis
| Pacote | Propósito |
|---|---|
@earendil-works/pi-coding-agent |
Tipos de extensão (ExtensionAPI, ExtensionContext, eventos) |
typebox |
Definições de esquema para parâmetros de ferramenta |
@earendil-works/pi-ai |
Utilitários de IA (StringEnum para enumerações compatíveis com o Google) |
@earendil-works/pi-tui |
TUI componentes para renderização personalizada |
npm dependências também funcionam. Adicione um package.json próximo à sua extensão (ou em um diretório pai), execute npm install e as importações de node_modules/ serão resolvidas automaticamente.
Para pacotes pi distribuídos instalados com pi install (npm ou git), as dependências de tempo de execução devem estar em dependencies. A instalação do pacote usa instalações de produção (npm install --omit=dev) por padrão, então devDependencies não estão disponíveis em tempo de execução; quando npmCommand é configurado, os pacotes git usam install simples para compatibilidade com wrappers.
Node.js integrados (node:fs, node:path, etc.) também estão disponíveis.
Escrevendo uma extensão
Uma extensão exporta uma função de fábrica padrão que recebe ExtensionAPI. A fábrica pode ser síncrona ou assíncrona:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// Subscribe to events
pi.on("event_name", async (event, ctx) => {
// ctx.ui for user interaction
const ok = await ctx.ui.confirm("Title", "Are you sure?");
ctx.ui.notify("Done!", "info");
ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)
});
// Register tools, commands, shortcuts, flags
pi.registerTool({ ... });
pi.registerCommand("name", { ... });
pi.registerShortcut("ctrl+x", { ... });
pi.registerFlag("my-flag", { ... });
}Extensions são carregados via jiti, então TypeScript funciona sem compilação.
Se a fábrica retornar Promise, pi aguardará antes de continuar a inicialização. Isso significa que a inicialização assíncrona é concluída antes de session_start, antes de resources_discover e antes que os registros do provedor enfileirados por meio de pi.registerProvider() sejam liberados.
Funções de fábrica assíncronas
Use uma fábrica assíncrona para trabalhos de inicialização únicos, como buscar configuração remota ou descobrir dinamicamente modelos disponíveis.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default async function (pi: ExtensionAPI) {
const response = await fetch("http://localhost:1234/v1/models");
const payload = (await response.json()) as {
data: Array<{
id: string;
name?: string;
context_window?: number;
max_tokens?: number;
}>;
};
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "$LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((model) => ({
id: model.id,
name: model.name ?? model.id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: model.context_window ?? 128000,
maxTokens: model.max_tokens ?? 4096,
})),
});
}Este padrão disponibiliza os modelos buscados durante a inicialização normal e para pi --list-models.
Recursos de longa duração e desligamento
As fábricas de extensão podem ser executadas em invocações que nunca iniciam uma sessão. Não inicie recursos em segundo plano, como processos, soquetes, observadores de arquivos ou temporizadores de fábrica.
Adie a inicialização do recurso em segundo plano até session_start ou o comando/ferramenta/evento que precisa do recurso. Registre um manipulador session_shutdown idempotente para fechar quaisquer recursos com escopo de sessão que você iniciar.
Estilos de extensão
Arquivo único - mais simples, para extensões pequenas:
~/.pi/agent/extensions/
└── my-extension.tsDiretório com index.ts – para extensões de vários arquivos:
~/.pi/agent/extensions/
└── my-extension/
├── index.ts # Entry point (exports default function)
├── tools.ts # Helper module
└── utils.ts # Helper modulePacote com dependências - para extensões que precisam de pacotes npm:
~/.pi/agent/extensions/
└── my-extension/
├── package.json # Declares dependencies and entry points
├── package-lock.json
├── node_modules/ # After npm install
└── src/
└── index.ts// package.json
{
"name": "my-extension",
"dependencies": {
"zod": "^3.0.0",
"chalk": "^5.0.0"
},
"pi": {
"extensions": ["./src/index.ts"]
}
}Execute npm install no diretório de extensão e as importações de node_modules/ funcionam automaticamente.
Eventos
Visão geral do ciclo de vida
pi starts
│
├─► project_trust (user/global and CLI extensions only, before project resources load)
├─► session_start { reason: "startup" }
└─► resources_discover { reason: "startup" }
│
▼
user sends prompt ─────────────────────────────────────────┐
│ │
├─► (extension commands checked first, bypass if found) │
├─► input (can intercept, transform, or handle) │
├─► (skill/template expansion if not handled) │
├─► before_agent_start (can inject message, modify system prompt)
├─► agent_start │
├─► message_start / message_update / message_end │
│ │
│ ┌─── turn (repeats while LLM calls tools) ───┐ │
│ │ │ │
│ ├─► turn_start │ │
│ ├─► context (can modify messages) │ │
│ ├─► before_provider_headers (can mutate headers) |
│ ├─► before_provider_request (can inspect or replace payload)
│ ├─► after_provider_response (status + headers, before stream consume)
│ │ │ │
│ │ LLM responds, may call tools: │ │
│ │ ├─► tool_execution_start │ │
│ │ ├─► tool_call (can block) │ │
│ │ ├─► tool_execution_update │ │
│ │ ├─► tool_result (can modify) │ │
│ │ └─► tool_execution_end │ │
│ │ │ │
│ └─► turn_end │ │
│ │
├─► agent_end │
└─► agent_settled (no retry/compaction/follow-up left) │
│
user sends another prompt ◄────────────────────────────────┘
/new (new session) or /resume (switch session)
├─► session_before_switch (can cancel)
├─► session_shutdown
├─► session_start { reason: "new" | "resume", previousSessionFile? }
└─► resources_discover { reason: "startup" }
/fork or /clone
├─► session_before_fork (can cancel)
├─► session_shutdown
├─► session_start { reason: "fork", previousSessionFile }
└─► resources_discover { reason: "startup" }
/name or pi.setSessionName()
└─► session_info_changed
/compact or auto-compaction
├─► session_before_compact (can cancel or customize)
└─► session_compact
/tree navigation
├─► session_before_tree (can cancel or customize)
└─► session_tree
/model or Ctrl+P (model selection/cycling)
├─► thinking_level_select (if model change changes/clamps thinking level)
└─► model_select
thinking level changes (settings, keybinding, pi.setThinkingLevel())
└─► thinking_level_select
exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
└─► session_shutdownEventos de inicialização
projeto_confiança
Disparado antes de pi decidir se deve confiar em um projeto com configurações dinâmicas (.pi ou .agents/skills). Ele é executado durante a inicialização e quando a substituição da sessão (por exemplo /resume) insere um cwd cuja confiança não foi resolvida no processo atual. Somente extensões de usuário/globais e extensões CLI -e participam; as extensões locais do projeto não são carregadas até que a confiança seja resolvida.
pi.on("project_trust", async (event, ctx) => {
// event.cwd - current working directory
// ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers
if (await ctx.ui.confirm("Trust project?", event.cwd)) {
return { trusted: "yes", remember: true };
}
return { trusted: "undecided" };
});Um manipulador project_trust deve retornar { trusted: "yes" | "no" | "undecided" }. Uma extensão de usuário/global ou CLI que retorna "yes" ou "no" possui a decisão; a primeira decisão sim/não vence e suprime o prompt de confiança integrado. Use remember: true para persistir uma decisão sim/não; caso contrário, aplica-se apenas ao processo atual. Retorne "undecided" para permitir que manipuladores posteriores ou o fluxo de confiança integrado decidam. Verifique ctx.hasUI antes de perguntar. Se nenhum manipulador retornar sim/não, a resolução de confiança normal continua: as decisões trust.json salvas se aplicam primeiro, então defaultProjectTrust controla se pi pergunta, confia ou recusa por padrão.
Eventos de recursos
recursos_descobrir
Disparado após session_start para que as extensões possam contribuir com habilidades adicionais, prompts e caminhos de tema.
O caminho de inicialização usa reason: "startup". Recarregar usa reason: "reload".
pi.on("resources_discover", async (event, _ctx) => {
// event.cwd - current working directory
// event.reason - "startup" | "reload"
return {
skillPaths: ["/path/to/skills"],
promptPaths: ["/path/to/prompts"],
themePaths: ["/path/to/themes"],
};
});Eventos de sessão
Consulte Session Format para informações internas de armazenamento de sessão e o SessionManager API.
sessão_início
Disparado quando uma sessão é iniciada, carregada ou recarregada.
pi.on("session_start", async (event, ctx) => {
// event.reason - "startup" | "reload" | "new" | "resume" | "fork"
// event.previousSessionFile - present for "new", "resume", and "fork"
ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
});session_info_changed
Disparado quando o nome de exibição da sessão atual é definido via /name, RPC ou pi.setSessionName().
pi.on("session_info_changed", async (event, ctx) => {
// event.name - current normalized name, or undefined if cleared
ctx.ui.notify(`Session renamed: ${event.name ?? "(none)"}`, "info");
});session_before_switch
Disparado antes de iniciar uma nova sessão (/new) ou trocar de sessão (/resume).
pi.on("session_before_switch", async (event, ctx) => {
// event.reason - "new" or "resume"
// event.targetSessionFile - session we're switching to (only for "resume")
if (event.reason === "new") {
const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
if (!ok) return { cancel: true };
}
});Após uma troca bem-sucedida ou ação de nova sessão, pi emite session_shutdown para a instância de extensão antiga, recarrega e religa as extensões para a nova sessão e, em seguida, emite session_start com reason: "new" | "resume" e previousSessionFile.
Faça o trabalho de limpeza em session_shutdown e restabeleça qualquer estado da memória em session_start.
sessão_before_fork
Disparado ao bifurcar via /fork ou clonar via /clone.
pi.on("session_before_fork", async (event, ctx) => {
// event.entryId - ID of the selected entry
// event.position - "before" for /fork, "at" for /clone
return { cancel: true }; // Cancel fork/clone
// OR
return { skipConversationRestore: true }; // Reserved for future conversation restore control
});Após uma bifurcação ou clonagem bem-sucedida, pi emite session_shutdown para a instância de extensão antiga, recarrega e religa as extensões para a nova sessão e, em seguida, emite session_start com reason: "fork" e previousSessionFile.
Faça o trabalho de limpeza em session_shutdown e restabeleça qualquer estado da memória em session_start.
session_before_compact / session_compact
Disparado na compactação. Veja compaction.md para detalhes.
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;
// reason - "manual" (/compact), "threshold", or "overflow"
// willRetry - whether the aborted turn is retried after compaction (overflow recovery)
// Cancel:
return { cancel: true };
// Custom summary:
return {
compaction: {
summary: "...",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
// usage: summaryResponse.usage, // Optional; included in session totals
}
};
});
pi.on("session_compact", async (event, ctx) => {
// event.compactionEntry - the saved compaction
// event.fromExtension - whether extension provided it
// event.reason - "manual" (/compact), "threshold", or "overflow"
// event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)
});session_before_tree / session_tree
Disparado na navegação /tree. Veja Sessions para conceitos de navegação em árvore.
pi.on("session_before_tree", async (event, ctx) => {
const { preparation, signal } = event;
return { cancel: true };
// OR provide custom summary:
return {
summary: {
summary: "...",
// usage: summaryResponse.usage, // Optional; included in session totals
details: {},
},
};
});
pi.on("session_tree", async (event, ctx) => {
// event.newLeafId, oldLeafId, summaryEntry, fromExtension
});sessão_desligamento
Disparado antes que o tempo de execução de uma sessão iniciada seja interrompido. Use isto para limpar recursos abertos em session_start ou outros ganchos com escopo de sessão.
pi.on("session_shutdown", async (event, ctx) => {
// event.reason - "quit" | "reload" | "new" | "resume" | "fork"
// event.targetSessionFile - destination session for session replacement flows
// Cleanup, save state, etc.
});Eventos do agente
antes_agente_start
Disparado após o usuário enviar o prompt, antes do loop do agente. Pode injetar uma mensagem e/ou modificar o prompt do sistema.
pi.on("before_agent_start", async (event, ctx) => {
// event.prompt - user's prompt text
// event.images - attached images (if any)
// event.systemPrompt - current chained system prompt for this handler
// (includes changes from earlier before_agent_start handlers)
// event.systemPromptOptions - structured options used to build the system prompt
// .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)
// .selectedTools - tools currently active in the prompt
// .toolSnippets - one-line descriptions for each tool
// .promptGuidelines - custom guideline bullets
// .appendSystemPrompt - text from --append-system-prompt flags
// .cwd - working directory
// .contextFiles - AGENTS.md files and other loaded context files
// .skills - loaded skills
return {
// Inject a persistent message (stored in session, sent to LLM)
message: {
customType: "my-extension",
content: "Additional context for the LLM",
display: true,
},
// Replace the system prompt for this turn (chained across extensions)
systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
};
});O campo systemPromptOptions dá às extensões acesso aos mesmos dados estruturados que Pi usa para construir o prompt do sistema. Isso permite inspecionar o que Pi foi carregado — prompts personalizados, diretrizes, trechos de ferramentas, context files, habilidades — sem redescobrir recursos ou analisar novamente sinalizadores. Use-o quando sua extensão precisar fazer alterações profundas e informadas no prompt do sistema, respeitando a configuração fornecida pelo usuário.
Dentro de before_agent_start, event.systemPrompt e ctx.getSystemPrompt() refletem o prompt do sistema encadeado do manipulador atual. Os manipuladores before_agent_start posteriores ainda podem modificá-lo novamente.
agente_start / agente_end / agente_settled
agent_start é acionado quando uma execução de agente de baixo nível começa. agent_end é acionado quando a execução termina, mas Pi ainda pode tentar novamente, compactar automaticamente e tentar novamente, ou continuar com mensagens de acompanhamento enfileiradas. Use agent_settled para integrações de status que precisam saber que Pi não continuarão sendo executadas automaticamente.
pi.on("agent_start", async (_event, ctx) => {});
pi.on("agent_end", async (event, ctx) => {
// event.messages - messages from this low-level run
});
pi.on("agent_settled", async (_event, ctx) => {
// ctx.isIdle() is true here unless another extension started a new run.
});turn_start / turn_end
Disparado a cada turno (uma resposta LLM + chamadas de ferramenta).
pi.on("turn_start", async (event, ctx) => {
// event.turnIndex, event.timestamp
});
pi.on("turn_end", async (event, ctx) => {
// event.turnIndex, event.message, event.toolResults
});mensagem_início / mensagem_atualização / mensagem_fim
Disparado para atualizações do ciclo de vida da mensagem.
message_startemessage_enddisparam para mensagens de usuário, assistente e toolResult.message_updatedispara para atualizações de streaming do assistente.- Os manipuladores
message_endpodem retornar{ message }para substituir a mensagem finalizada. A substituição deve manter o mesmorole.
pi.on("message_start", async (event, ctx) => {
// event.message
});
pi.on("message_update", async (event, ctx) => {
// event.message
// event.assistantMessageEvent (token-by-token stream event)
});
pi.on("message_end", async (event, ctx) => {
if (event.message.role !== "assistant") return;
return {
message: {
...event.message,
usage: {
...event.message.usage,
cost: {
...event.message.usage.cost,
total: 0.123,
},
},
},
};
});tool_execution_start / tool_execution_update / tool_execution_end
Disparado para atualizações do ciclo de vida de execução da ferramenta.
No modo de ferramenta paralela:
tool_execution_starté emitido na ordem da fonte assistente durante a fase de comprovaçãotool_execution_updateeventos podem intercalar-se entre ferramentastool_execution_endé emitido na ordem de conclusão da ferramenta após cada ferramenta ser finalizada- eventos de mensagem final
toolResultainda são emitidos posteriormente na ordem de origem do assistente
pi.on("tool_execution_start", async (event, ctx) => {
// event.toolCallId, event.toolName, event.args
});
pi.on("tool_execution_update", async (event, ctx) => {
// event.toolCallId, event.toolName, event.args, event.partialResult
});
pi.on("tool_execution_end", async (event, ctx) => {
// event.toolCallId, event.toolName, event.result, event.isError
});contexto
Disparado antes de cada chamada LLM. Modifique mensagens de forma não destrutiva. Veja Session Format para tipos de mensagens.
pi.on("context", async (event, ctx) => {
// event.messages - deep copy, safe to modify
const filtered = event.messages.filter(m => !shouldPrune(m));
return { messages: filtered };
});before_provider_headers
Disparado após a montagem dos cabeçalhos HTTP de saída. Use-o para adicionar, substituir ou remover cabeçalhos de solicitação.
Os manipuladores mudam event.headers no lugar. Defina uma chave para uma string para adicioná-la ou substituí-la, ou para null para excluí-la.
pi.on("before_provider_headers", (event, ctx) => {
// Add or override — e.g. a session id for gateway tracing/attribution
event.headers["x-session-id"] = ctx.sessionManager.getSessionId();
// Drop a tracking header pi adds for this call
event.headers["X-OpenRouter-Title"] = null;
});Executa uma vez por solicitação do provedor; tenta reutilizar os mesmos cabeçalhos em vez de disparar novamente o gancho.
before_provider_request
Disparado após a construção da carga específica do provedor, logo antes do envio da solicitação. Os manipuladores são executados na ordem de carregamento da extensão. Retornar undefined mantém a carga inalterada. Retornar qualquer outro valor substitui a carga útil para manipuladores posteriores e para a solicitação real.
Este gancho pode reescrever as instruções do sistema no nível do provedor ou removê-las completamente. Essas alterações no nível da carga útil não são refletidas por ctx.getSystemPrompt(), que relata a string de prompt do sistema Pi em vez da carga útil final do provedor serializado.
pi.on("before_provider_request", (event, ctx) => {
console.log(JSON.stringify(event.payload, null, 2));
// Optional: replace payload
// return { ...event.payload, temperature: 0 };
});Isso é útil principalmente para depurar a serialização do provedor e o comportamento do cache.
after_provider_response
Disparado depois que uma resposta HTTP é recebida e antes que seu corpo de fluxo seja consumido. Os manipuladores são executados na ordem de carregamento da extensão.
pi.on("after_provider_response", (event, ctx) => {
// event.status - HTTP status code
// event.headers - normalized response headers
if (event.status === 429) {
console.log("rate limited", event.headers["retry-after"]);
}
});A disponibilidade do cabeçalho depende do fornecedor e do transporte. Providers que respostas HTTP abstratas não podem expor cabeçalhos.
Eventos Modelo
seleção_modelo
Disparado quando o modelo é alterado por meio do comando /model, ciclo de modelo (Ctrl+P) ou restauração de sessão.
pi.on("model_select", async (event, ctx) => {
// event.model - newly selected model
// event.previousModel - previous model (undefined if first selection)
// event.source - "set" | "cycle" | "restore"
const prev = event.previousModel
? `${event.previousModel.provider}/${event.previousModel.id}`
: "none";
const next = `${event.model.provider}/${event.model.id}`;
ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");
});Use isto para atualizar elementos da UI (barras de status, rodapés) ou executar inicialização específica do modelo quando o modelo ativo for alterado.
pensando_nível_select
Disparado quando o nível de pensamento muda. Isso é apenas para notificação; os valores de retorno do manipulador são ignorados.
pi.on("thinking_level_select", async (event, ctx) => {
// event.level - newly selected thinking level
// event.previousLevel - previous thinking level
ctx.ui.setStatus("thinking", `thinking: ${event.level}`);
});Use isto para atualizar a interface do usuário da extensão quando pi.setThinkingLevel(), alterações de modelo ou controles de nível de pensamento integrados alteram o nível de pensamento ativo.
Eventos de ferramentas
ferramenta_call
Disparado após tool_execution_start, antes da execução da ferramenta. Pode bloquear. Use isToolCallEventType para restringir e obter entradas digitadas.
Antes da execução de tool_call, pi espera que os eventos do Agente emitidos anteriormente terminem de ser drenados por AgentSession. Isso significa que ctx.sessionManager está atualizado por meio da mensagem atual de chamada da ferramenta do assistente.
No modo de execução de ferramenta paralela padrão, as chamadas de ferramentas irmãs da mesma mensagem do assistente são pré-flightadas sequencialmente e, em seguida, executadas simultaneamente. Não é garantido que tool_call veja os resultados da ferramenta irmã da mesma mensagem do assistente em ctx.sessionManager.
event.input é mutável. Mude-o para corrigir os argumentos da ferramenta antes da execução.
Garantias de comportamento:
- Mutações para
event.inputafetam a execução real da ferramenta - Os manipuladores
tool_callposteriores veem as mutações feitas pelos manipuladores anteriores - Nenhuma revalidação é realizada após sua mutação
- Retornar valores do bloqueio de controle
tool_callvia{ block: true, reason?: string, terminate?: boolean } terminateaplica-se apenas a uma chamada bloqueada; o agente para mais cedo somente quando todos os resultados finalizados no lote estão terminando
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
pi.on("tool_call", async (event, ctx) => {
// event.toolName - "bash", "read", "write", "edit", etc.
// event.toolCallId
// event.input - tool parameters (mutable)
// Built-in tools: no type params needed
if (isToolCallEventType("bash", event)) {
// event.input is { command: string; timeout?: number }
event.input.command = `source ~/.profile\n${event.input.command}`;
if (event.input.command.includes("rm -rf")) {
return { block: true, reason: "Dangerous command", terminate: true };
}
}
if (isToolCallEventType("read", event)) {
// event.input is { path: string; offset?: number; limit?: number }
console.log(`Reading: ${event.input.path}`);
}
});Digitando entrada de ferramenta personalizada
As ferramentas personalizadas devem exportar seu tipo de entrada:
// my-extension.ts
export type MyToolInput = Static<typeof myToolSchema>;Use isToolCallEventType com parâmetros de tipo explícitos:
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
import type { MyToolInput } from "my-extension";
pi.on("tool_call", (event) => {
if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
event.input.action; // typed
}
});resultado_ferramenta
Disparado após o término da execução da ferramenta e antes de tool_execution_end mais os eventos de mensagem de resultado final da ferramenta serem emitidos. Pode modificar o resultado.
No modo de ferramenta paralela, tool_result e tool_execution_end podem intercalar na ordem de conclusão da ferramenta, enquanto os eventos de mensagem toolResult finais ainda são emitidos posteriormente na ordem de origem do assistente.
tool_result cadeia de manipuladores como middleware:
- Os manipuladores são executados na ordem de carregamento da extensão
- Cada manipulador vê o resultado mais recente após alterações anteriores no manipulador
- Os manipuladores podem retornar patches parciais (
content,details,isErrorouusage); campos omitidos mantêm seus valores atuais
Use ctx.signal para trabalho assíncrono aninhado dentro do manipulador. Isso permite que Esc cancele chamadas de modelo, fetch(), e outras operações com reconhecimento de aborto iniciadas pela extensão.
import { isBashToolResult } from "@earendil-works/pi-coding-agent";
pi.on("tool_result", async (event, ctx) => {
// event.toolName, event.toolCallId, event.input
// event.content, event.details, event.isError, event.usage
if (isBashToolResult(event)) {
// event.details is typed as BashToolDetails
}
const response = await fetch("https://example.com/summarize", {
method: "POST",
body: JSON.stringify({ content: event.content }),
signal: ctx.signal,
});
// Modify result:
return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };
});Eventos Bash do usuário
usuário_bash
Disparado quando o usuário executa os comandos ! ou !!. Pode interceptar.
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
pi.on("user_bash", (event, ctx) => {
// event.command - the bash command
// event.excludeFromContext - true if !! prefix
// event.cwd - working directory
// Option 1: Provide custom operations (e.g., SSH)
return { operations: remoteBashOps };
// Option 2: Wrap pi's built-in local bash backend
const local = createLocalBashOperations();
return {
operations: {
exec(command, cwd, options) {
return local.exec(`source ~/.profile\n${command}`, cwd, options);
}
}
};
// Option 3: Full replacement - return result directly
return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
});Eventos de entrada
entrada
Disparado quando a entrada do usuário é recebida, após a verificação dos comandos de extensão, mas antes da expansão da habilidade e do modelo. O evento vê o texto de entrada bruto, portanto /skill:foo e /template ainda não foram expandidos.
Ordem de processamento:
- Comandos de extensão (
/cmd) verificados primeiro - se encontrados, o manipulador é executado e o evento de entrada é ignorado inputeventos disparados - podem interceptar, transformar ou manipular- Se não for tratado: comandos de habilidade (
/skill:name) expandidos para conteúdo de habilidade - Se não for tratado: prompt templates (
/template) expandido para o conteúdo do modelo - O processamento do agente começa (
before_agent_start, etc.)
pi.on("input", async (event, ctx) => {
// event.text - raw input (before skill/template expansion)
// event.images - attached images, if any
// event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
// event.streamingBehavior - "steer" | "followUp" | undefined
// undefined when idle, "steer" for mid-stream interrupts,
// "followUp" for messages queued until the agent finishes
// Transform: rewrite input before expansion
if (event.text.startsWith("?quick "))
return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };
// Handle: respond without LLM (extension shows its own feedback)
if (event.text === "ping") {
ctx.ui.notify("pong", "info");
return { action: "handled" };
}
// Route by source: skip processing for extension-injected messages
if (event.source === "extension") return { action: "continue" };
// Intercept skill commands before expansion
if (event.text.startsWith("/skill:")) {
// Could transform, block, or let pass through
}
return { action: "continue" }; // Default: pass through to expansion
});Resultados:
continue- passa inalterado (padrão se o manipulador não retornar nada)transform- modifique texto/imagens e continue a expansãohandled- ignora totalmente o agente (o primeiro manipulador a retornar vence)
Transforma a cadeia entre manipuladores. Consulte input-transform.ts e input-transform-streaming.ts para roteamento com reconhecimento de streamingBehavior.
ExtensãoContexto
Todos os manipuladores recebem ctx: ExtensionContext.
ctx.ui
Métodos de UI para interação do usuário. Veja Custom UI para detalhes completos.
ctx.modo
Modo de execução atual: "tui", "rpc", "json" ou "print". Use ctx.mode === "tui" para proteger recursos somente de terminal, como custom(), fábricas de componentes, entrada de terminal e renderização direta de TUI.
ctx.hasUI
true nos modos TUI e RPC. false no modo de impressão (-p) e no modo JSON. Use isto para proteger métodos de diálogo (select, confirm, input, editor) e métodos de disparar e esquecer (notify, setStatus, setWidget, setTitle, setEditorText) que funcionam em TUI e RPC modos. No modo RPC, alguns métodos específicos de TUI são autônomos ou retornam padrões (consulte rpc.md).
ctx.cwd
Diretório de trabalho atual.
Use CONFIG_DIR_NAME em vez de codificar .pi ao construir caminhos de configuração local do projeto. Distribuições renomeadas podem usar um nome de diretório de configuração diferente.
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { join } from "node:path";
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json");
// ...
});
}ctx.isProjectTrusted()
Retorna se a confiança local do projeto está ativa para o contexto da sessão atual. Isso inclui decisões de confiança temporárias e substituições de confiança CLI, não apenas decisões salvas no armazenamento confiável global.
Use isto antes de ler a configuração da extensão local do projeto que só deve ser respeitada para projetos confiáveis.
ctx.sessionManager
Acesso somente leitura ao estado da sessão. Veja Session Format para o SessionManager API completo e tipos de entrada.
Para tool_call, esse estado é sincronizado por meio da mensagem do assistente atual antes da execução dos manipuladores. No modo de execução de ferramenta paralela ainda não é garantido que inclua resultados de ferramentas irmãs da mesma mensagem do assistente.
ctx.sessionManager.getEntries() // All entries
ctx.sessionManager.getBranch() // Current branch
ctx.sessionManager.buildContextEntries() // Active branch entries with compaction applied
ctx.sessionManager.getLeafId() // Current leaf entry IDctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels
Acesso a modelos, provedores e autenticação resolvida. ctx.modelRegistry.getProvider(id) retorna o provedor pi-ai efetivo, enquanto getProviderAuth(id) resolve seu API key atual, cabeçalhos, URL base e ambiente com escopo do provedor sem exigir um modelo carregado. ctx.model é o modelo ativo e ctx.thinkingLevel é seu atual nível de pensamento efetivo.
ctx.scopedModels é a lista somente leitura de modelos com escopo definido para a sessão atual — o mesmo conjunto que o comando /scoped-models mostra. É resolvido no início da sessão a partir do sinalizador --models CLI e da configuração enabledModels (comparado com o catálogo disponível com minimatch em provider/modelId ou um modelId simples). Fica vazio quando nenhum escopo está configurado, o que significa que todos os modelos disponíveis podem ser usados. Cada entrada é { model, thinkingLevel? }, onde thinkingLevel é definido apenas quando um padrão a fixa (por exemplo, anthropic/*:high). Use-o para preencher um seletor de modelo que espelhe o integrado em vez de enumerar todo o catálogo via ctx.modelRegistry.getAvailable().
ctx.signal
O sinal de aborto do agente atual, ou undefined quando nenhum turno do agente está ativo.
Use isto para trabalhos aninhados com reconhecimento de interrupção iniciados por manipuladores de extensão, por exemplo:
fetch(..., { signal: ctx.signal })- chamadas de modelo que aceitam
signal - auxiliares de arquivo ou processo que aceitam
AbortSignal
ctx.signal é normalmente definido durante eventos de turno ativo, como tool_call, tool_result, message_update e turn_end.
Geralmente é undefined em contextos ociosos ou sem turno, como eventos de sessão, comandos de extensão e atalhos disparados enquanto pi está ocioso.
pi.on("tool_result", async (event, ctx) => {
const response = await fetch("https://example.com/api", {
method: "POST",
body: JSON.stringify(event),
signal: ctx.signal,
});
const data = await response.json();
return { details: data };
});ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()
Auxiliares de fluxo de controle. ctx.isIdle() é falso enquanto Pi está processando uma execução de agente, nova tentativa automática, nova tentativa de compactação automática ou continuação na fila.
ctx.shutdown()
Solicite um desligamento normal do pi.
- Modo interativo: Adiado até que o agente fique ocioso (depois de processar todas as mensagens de orientação e acompanhamento enfileiradas).
- Modo RPC: Adiado até o próximo estado inativo (após completar a resposta do comando atual, ao aguardar o próximo comando).
- Modo de impressão: No-op. O processo é encerrado automaticamente quando todos os prompts são processados.
Emite o evento session_shutdown para todas as extensões antes de sair. Disponível em todos os contextos (manipuladores de eventos, ferramentas, comandos, atalhos).
pi.on("tool_call", (event, ctx) => {
if (isFatal(event.input)) {
ctx.shutdown();
}
});ctx.getContextUsage()
Retorna o uso do contexto atual para o modelo ativo. Usa o último uso do assistente quando disponível e, em seguida, estima tokens para mensagens finais.
const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
// ...
}ctx.compact()
Acione a compactação sem aguardar a conclusão. Use onComplete e onError para ações de acompanhamento.
ctx.compact({
customInstructions: "Focus on recent changes",
onComplete: (result) => {
ctx.ui.notify("Compaction completed", "info");
},
onError: (error) => {
ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
},
});ctx.getSystemPrompt()
Retorna a string de prompt do sistema atual de Pi.
- Durante
before_agent_start, isso reflete as alterações encadeadas no prompt do sistema feitas até agora para o turno atual. - Não inclui mutações de mensagens
contextposteriores. - Não inclui reescritas de carga útil
before_provider_request. - Se as extensões carregadas posteriormente forem executadas depois das suas, elas ainda poderão alterar o que será enviado.
pi.on("before_agent_start", (event, ctx) => {
const prompt = ctx.getSystemPrompt();
console.log(`System prompt length: ${prompt.length}`);
});ExtensãoCommandContext
Os manipuladores de comando recebem ExtensionCommandContext, que estende ExtensionContext com métodos de controle de sessão. Eles estão disponíveis apenas em comandos porque podem travar se chamados a partir de manipuladores de eventos.
ctx.getSystemPromptOptions()
Retorna as entradas básicas Pi usadas atualmente para construir o prompt do sistema.
const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];Tem a mesma forma e mutabilidade que before_agent_start event.systemPromptOptions: prompt personalizado, ferramentas ativas, trechos de ferramentas, diretrizes de prompt, texto de prompt do sistema anexado, cwd, context files carregado e habilidades carregadas. Ele pode incluir o conteúdo completo do arquivo de contexto, portanto, trate-o como dados confidenciais de extensão local e evite expô-lo por meio de listas de comandos, logs ou metadados de preenchimento automático.
Isso relata as entradas atuais do prompt de base. Não inclui alterações de prompt do sistema encadeadas before_agent_start por turno, mutações posteriores de mensagens de evento context ou reescritas de carga útil before_provider_request.
ctx.waitForIdle()
Aguarde até que o agente seja totalmente liquidado, incluindo novas tentativas automáticas, novas tentativas de compactação automática e continuações na fila:
pi.registerCommand("my-cmd", {
handler: async (args, ctx) => {
await ctx.waitForIdle();
// Agent is now idle, safe to modify session
},
});ctx.newSession(opções?)
Crie uma nova sessão:
const parentSession = ctx.sessionManager.getSessionFile();
const kickoff = "Continue in the replacement session";
const result = await ctx.newSession({
parentSession,
setup: async (sm) => {
sm.appendMessage({
role: "user",
content: [{ type: "text", text: "Context from previous session..." }],
timestamp: Date.now(),
});
},
withSession: async (ctx) => {
// Use only the replacement-session ctx here.
await ctx.sendUserMessage(kickoff);
},
});
if (result.cancelled) {
// An extension cancelled the new session
}Opções:
parentSession: arquivo da sessão pai para gravar no novo cabeçalho da sessãosetup: altera oSessionManagerda nova sessão antes dewithSessionser executadowithSession: execute o trabalho pós-troca em um novo contexto de sessão de substituição. Não use o antigopi/ comandoctxcapturado; veja Session replacement lifecycle and footguns.
ctx.fork(entryId, opções?)
Bifurque uma entrada específica, criando um novo arquivo de sessão:
const result = await ctx.fork("entry-id-123", {
withSession: async (ctx) => {
// Use only the replacement-session ctx here.
ctx.ui.notify("Now in the forked session", "info");
},
});
if (result.cancelled) {
// An extension cancelled the fork
}
const cloneResult = await ctx.fork("entry-id-456", { position: "at" });
if (cloneResult.cancelled) {
// An extension cancelled the clone
}Opções:
position:"before"(padrão) bifurca-se antes da mensagem do usuário selecionado, restaurando esse prompt no editorposition:"at"duplica o caminho ativo através da entrada selecionada sem restaurar o texto do editorwithSession: execute o trabalho pós-troca em um novo contexto de sessão de substituição. Não use o antigopi/ comandoctxcapturado; veja Session replacement lifecycle and footguns.
ctx.navigateTree(targetId, opções?)
Navegue para um ponto diferente no session tree:
const result = await ctx.navigateTree("entry-id-456", {
summarize: true,
customInstructions: "Focus on error handling changes",
replaceInstructions: false, // true = replace default prompt entirely
label: "review-checkpoint",
});Opções:
summarize: Se deve gerar um resumo da filial abandonadacustomInstructions: Instruções personalizadas para o resumidorreplaceInstructions: Se verdadeiro,customInstructionssubstitui o prompt padrão em vez de ser anexadolabel: Etiqueta a ser anexada à entrada de resumo da ramificação (ou entrada de destino, se não estiver resumindo)
ctx.switchSession(sessionPath, opções?)
Mude para um arquivo de sessão diferente:
const result = await ctx.switchSession("/path/to/session.jsonl", {
withSession: async (ctx) => {
await ctx.sendUserMessage("Resume work in the replacement session");
},
});
if (result.cancelled) {
// An extension cancelled the switch via session_before_switch
}Opções:
withSession: execute o trabalho pós-troca em um novo contexto de sessão de substituição. Não use o antigopi/ comandoctxcapturado; veja Session replacement lifecycle and footguns.
Para descobrir sessões disponíveis, use os métodos estáticos SessionManager.list() ou SessionManager.listAll():
import { SessionManager } from "@earendil-works/pi-coding-agent";
pi.registerCommand("switch", {
description: "Switch to another session",
handler: async (args, ctx) => {
const sessions = await SessionManager.list(ctx.cwd);
if (sessions.length === 0) return;
const choice = await ctx.ui.select(
"Pick session:",
sessions.map(s => s.file),
);
if (choice) {
await ctx.switchSession(choice, {
withSession: async (ctx) => {
ctx.ui.notify("Switched session", "info");
},
});
}
},
});Ciclo de vida de substituição de sessão e armas de pé
withSession recebe um novo ReplacedSessionContext, que estende ExtensionCommandContext com auxiliares assíncronos sendMessage() e sendUserMessage() vinculados à sessão de substituição.
Ciclo de vida e armas de pé:
withSessioné executado somente depois que a sessão antiga emitiusession_shutdown, o tempo de execução antigo foi interrompido, a sessão de substituição foi recuperada e a nova instância de extensão já recebeusession_start.- O retorno de chamada ainda é executado no encerramento original, não dentro da nova instância de extensão. Isso significa que sua antiga instância de extensão já pode ter executado a limpeza de desligamento antes de
withSessioniniciar. - Objetos antigos
pi/ comando antigoctxcapturados e vinculados à sessão ficam obsoletos após a substituição e serão lançados se usados. Use apenasctxpassado parawithSessionpara trabalho vinculado à sessão. - Objetos brutos extraídos anteriormente ainda são de sua responsabilidade. Por exemplo, se você capturar
const sm = ctx.sessionManagerantes da substituição,smainda será o antigo objetoSessionManager. Não o reutilize após a substituição. - O código em
withSessiondeve assumir que qualquer estado invalidado pelo seu manipuladorsession_shutdownjá desapareceu. Capture apenas dados simples que sobrevivem ao desligamento de forma limpa, como strings, ids e configuração serializada.
Padrão seguro:
pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const kickoff = "Continue from the replacement session";
await ctx.newSession({
withSession: async (ctx) => {
await ctx.sendUserMessage(kickoff);
},
});
},
});Padrão inseguro:
pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const oldSessionManager = ctx.sessionManager;
await ctx.newSession({
withSession: async (_ctx) => {
// stale old objects: do not do this
oldSessionManager.getSessionFile();
pi.sendUserMessage("wrong");
},
});
},
});ctx.reload()
Execute o mesmo fluxo de recarga de /reload.
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});Comportamento importante:
await ctx.reload()emitesession_shutdownpara o tempo de execução da extensão atual- Em seguida, recarrega recursos e emite
session_startcomreason: "reload"eresources_discovercom motivo"reload" - O manipulador de comandos atualmente em execução ainda continua no quadro de chamada antigo
- O código após
await ctx.reload()ainda é executado na versão pré-recarregamento - O código após
await ctx.reload()não deve assumir que o estado antigo da extensão na memória ainda é válido - Após o retorno do manipulador, futuros comandos/eventos/chamadas de ferramentas usarão a nova versão da extensão
Para um comportamento previsível, trate reload como terminal para esse manipulador (await ctx.reload(); return;).
As ferramentas são executadas com ExtensionContext, portanto não podem chamar ctx.reload() diretamente. Use um comando como ponto de entrada de recarga e, em seguida, exponha uma ferramenta que coloque esse comando na fila como uma mensagem de acompanhamento do usuário.
Exemplo de ferramenta que o LLM pode chamar para acionar o recarregamento:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});
pi.registerTool({
name: "reload_runtime",
label: "Reload Runtime",
description: "Reload extensions, skills, prompts, themes, and context files",
parameters: Type.Object({}),
async execute() {
pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
return {
content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
};
},
});
}ExtensãoAPI Métodos
pi.on(evento, manipulador)
Inscreva-se em eventos. Consulte Events para tipos de eventos e valores de retorno.
pi.registerTool (definição)
Registre uma ferramenta personalizada que pode ser chamada pelo LLM. Veja Custom Tools para detalhes completos.
pi.registerTool() funciona durante o carregamento da extensão e após a inicialização. Você pode chamá-lo dentro de session_start, manipuladores de comandos ou outros manipuladores de eventos. Novas ferramentas são atualizadas imediatamente na mesma sessão, então elas aparecem em pi.getAllTools() e podem ser chamadas pelo LLM sem /reload.
Use pi.setActiveTools() para ativar ou desativar ferramentas (incluindo ferramentas adicionadas dinamicamente) em tempo de execução.
Use promptSnippet para incluir uma ferramenta personalizada em uma entrada de uma linha em Available tools e promptGuidelines para anexar marcadores específicos da ferramenta à seção Guidelines padrão quando a ferramenta estiver ativa.
Importante: os marcadores promptGuidelines são anexados na seção Guidelines sem prefixo de nome de ferramenta. Cada diretriz deve nomear a ferramenta a que se refere - evite "Use esta ferramenta quando..." porque o LLM não pode dizer qual ferramenta "isto" significa. Escreva "Use my_tool quando...".
Veja dynamic-tools.ts para um exemplo completo.
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does",
promptSnippet: "Summarize or transform text according to action",
promptGuidelines: ["Use my_tool when the user asks to summarize previously generated text."],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const),
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
// Optional compatibility shim. Runs before schema validation.
// Return the current schema shape, for example to fold legacy fields
// into the modern parameter object.
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Stream progress
onUpdate?.({ content: [{ type: "text", text: "Working..." }] });
return {
content: [{ type: "text", text: "Done" }],
details: { result: "..." },
};
},
// Optional: Custom rendering
renderCall(args, theme, context) { ... },
renderResult(result, options, theme, context) { ... },
});pi.sendMessage(mensagem, opções?)
Injete uma mensagem personalizada na sessão. Mensagens personalizadas participam do contexto LLM. Para conteúdo durável apenas TUI que não deve ser enviado para o LLM, use pi.appendEntry() com pi.registerEntryRenderer().
pi.sendMessage({
customType: "my-extension",
content: "Message text",
display: true,
details: { ... },
}, {
triggerTurn: true,
deliverAs: "steer",
});Opções:
deliverAs- Modo de entrega:"steer"(padrão) – Coloca a mensagem na fila durante o streaming. Entregue após o turno do assistente atual terminar de executar suas chamadas de ferramenta, antes da próxima chamada do LLM."followUp"- Espera o agente terminar. Entregue somente quando o agente não tiver mais chamadas de ferramenta."nextTurn"- Na fila para o próximo prompt do usuário. Não interrompe nem desencadeia nada.
triggerTurn: true- Se o agente estiver ocioso, acione uma resposta LLM imediatamente. Aplica-se apenas aos modos"steer"e"followUp"(ignorado para"nextTurn").
pi.sendUserMessage(conteúdo, opções?)
Envie uma mensagem do usuário ao agente. Ao contrário de sendMessage() que envia mensagens personalizadas, este envia uma mensagem real do usuário que aparece como se tivesse sido digitada pelo usuário. Sempre aciona um turno.
// Simple text message
pi.sendUserMessage("What is 2+2?");
// With content array (text + images)
pi.sendUserMessage([
{ type: "text", text: "Describe this image:" },
{ type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },
]);
// During streaming - must specify delivery mode
pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });Opções:
deliverAs- Obrigatório quando o agente está transmitindo:"steer"- Coloca a mensagem na fila para entrega após o turno atual do assistente terminar de executar suas chamadas de ferramenta"followUp"- Espera o agente terminar todas as ferramentas
Quando não está transmitindo, a mensagem é enviada imediatamente e aciona um novo turno. Ao transmitir sem deliverAs, gera um erro.
Veja send-user-message.ts para um exemplo completo.
pi.appendEntry(customType, dados?)
Persistir dados de extensão. As entradas personalizadas NÃO participam do contexto LLM. No modo interativo, eles também podem ser renderizados dentro da transcrição do bate-papo quando combinados com pi.registerEntryRenderer().
pi.appendEntry("my-state", { count: 42 });
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });
// Restore on reload
pi.on("session_start", async (_event, ctx) => {
for (const entry of ctx.sessionManager.getEntries()) {
if (entry.type === "custom" && entry.customType === "my-state") {
// Reconstruct from entry.data
}
}
});pi.setSessionName(nome)
Defina o nome de exibição da sessão (mostrado no seletor de sessão em vez da primeira mensagem).
pi.setSessionName("Refactor auth module");pi.getSessionName()
Obtenha o nome da sessão atual, se definido.
const name = pi.getSessionName();
if (name) {
console.log(`Session: ${name}`);
}pi.setLabel(entryId, rótulo)
Defina ou desmarque um rótulo em uma entrada. Etiquetas são marcadores definidos pelo usuário para marcação e navegação (mostrados no seletor /tree).
// Set a label
pi.setLabel(entryId, "checkpoint-before-refactor");
// Clear a label
pi.setLabel(entryId, undefined);
// Read labels via sessionManager
const label = ctx.sessionManager.getLabel(entryId);Os rótulos persistem na sessão e sobrevivem às reinicializações. Use-os para marcar pontos importantes (curvas, pontos de controle) na árvore de conversação.
pi.registerCommand(nome, opções)
Registre um comando.
Se múltiplas extensões registrarem o mesmo nome de comando, pi mantém todas elas e atribui sufixos de invocação numérica na ordem de carregamento, por exemplo /review:1 e /review:2.
pi.registerCommand("stats", {
description: "Show session statistics",
handler: async (args, ctx) => {
const count = ctx.sessionManager.getEntries().length;
ctx.ui.notify(`${count} entries`, "info");
}
});Opcional: adicione preenchimento automático de argumento para /command...:
import type { AutocompleteItem } from "@earendil-works/pi-tui";
pi.registerCommand("deploy", {
description: "Deploy to an environment",
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
const envs = ["dev", "staging", "prod"];
const items = envs.map((e) => ({ value: e, label: e }));
const filtered = items.filter((i) => i.value.startsWith(prefix));
return filtered.length > 0 ? filtered : null;
},
handler: async (args, ctx) => {
ctx.ui.notify(`Deploying: ${args}`, "info");
},
});pi.getCommands()
Obtenha o slash commands disponível para invocação via prompt na sessão atual. Inclui comandos de extensão, prompt templates e comandos de habilidade.
A lista corresponde à ordem RPC get_commands: primeiro as extensões, depois os modelos e depois as habilidades.
const commands = pi.getCommands();
const bySource = commands.filter((command) => command.source === "extension");
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");Cada entrada tem este formato:
{
name: string; // Invokable command name without the leading slash. May be suffixed like "review:1"
description?: string;
source: "extension" | "prompt" | "skill";
sourceInfo: {
path: string;
source: string;
scope: "user" | "project" | "temporary";
origin: "package" | "top-level";
baseDir?: string;
};
}Use sourceInfo como campo de proveniência canônica. Não infira a propriedade a partir de nomes de comandos ou de análise de caminho ad hoc.
Comandos interativos integrados (como /model e /settings) não estão incluídos aqui. Eles são tratados apenas de forma interativa
modo e não seria executado se enviado via prompt.
pi.registerMessageRenderer(customType, renderizador)
Registre um renderizador TUI personalizado para mensagens personalizadas com seu customType. Mensagens personalizadas são criadas com pi.sendMessage() e participam do contexto LLM. Consulte Custom UI.
pi.registerMarkdownTransformador(transformador)
Registre um transformador para Markdown em texto normal do usuário, texto assistente e blocos de pensamento. Os transformadores são executados em ordem de carregamento de extensão e cada transformador recebe o Markdown retornado pelo transformador anterior. Após o término da cadeia, Pi renderiza o conteúdo transformado com seu renderizador integrado.
O transformador recebe a string Markdown e um contexto com:
messageType—"user","assistant"ou"assistant-thinking"isStreaming—truepara atualizações parciais do assistente;falsepara usuário, assistente finalizado e mensagens restauradasavailableWidth— colunas terminais exatas disponíveis para o conteúdo Markdown transformado
Retorne o transformado Markdown:
pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
if (isStreaming || messageType === "assistant-thinking") return markdown;
return markdown.replaceAll("-->", "→");
});Se um transformador for acionado, Pi mantém o Markdown produzido até agora e continua com o próximo transformador. O gancho é somente para exibição: a mensagem original permanece inalterada no contexto da sessão e do modelo. Ele é executado para mensagens de novos usuários, atualizações de streaming do assistente, mensagens de sessão restauradas e alterações na largura do terminal, portanto, os transformadores devem permanecer síncronos e baratos.
pi.registerEntryRenderer(customType, renderizador)
Registre um renderizador TUI personalizado para entradas personalizadas com seu customType. As entradas personalizadas são criadas com pi.appendEntry() e não participam do contexto LLM.
import { Box, Text } from "@earendil-works/pi-tui";
pi.registerEntryRenderer("status-card", (entry, { expanded }, theme) => {
const data = entry.data as { title: string; count: number };
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));
if (expanded) {
box.addChild(new Text(theme.fg("dim", JSON.stringify(data, null, 2))));
}
return box;
});
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });pi.registerShortcut(atalho, opções)
Registre um atalho de teclado. Veja keybindings.md para o formato do atalho e atalhos de teclado integrados.
pi.registerShortcut("ctrl+shift+p", {
description: "Toggle plan mode",
handler: async (ctx) => {
ctx.ui.notify("Toggled!");
},
});pi.registerFlag(nome, opções)
Registre um sinalizador CLI.
pi.registerFlag("plan", {
description: "Start in plan mode",
type: "boolean",
default: false,
});
// Check value
if (pi.getFlag("plan")) {
// Plan mode enabled
}pi.exec(comando, argumentos, opções?)
Execute um comando shell.
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killedpi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(nomes)
Gerenciar ferramentas ativas. Isso funciona tanto para ferramentas integradas quanto para ferramentas registradas dinamicamente. pi.getActiveTools() retorna os nomes das ferramentas ativas como string[]; pi.getAllTools() retorna metadados para todas as ferramentas configuradas.
const active = pi.getActiveTools(); // ["read", "bash", ...]
const all = pi.getAllTools();
// all = [{
// name: "read",
// description: "Read file contents...",
// parameters: ...,
// promptGuidelines: ["Use read to examine files instead of cat or sed."],
// sourceInfo: { path: "<builtin:read>", source: "builtin", scope: "temporary", origin: "top-level" }
// }, ...]
const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin");
const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk");
pi.setActiveTools([...new Set([...active, "my_custom_tool"])]); // Keep current tools and enable my_custom_tool
pi.setActiveTools(["read", "bash"]); // Switch to read-onlypi.getAllTools() retorna name, description, parameters, promptGuidelines e sourceInfo.
Valores típicos de sourceInfo.source:
builtinpara ferramentas integradassdkpara ferramentas passadas viacreateAgentSession({ customTools })- metadados de origem de extensão para ferramentas registradas por extensões
pi.setModel(modelo)
Defina o modelo atual. Retorna false se nenhum API key estiver disponível para o modelo. Consulte models.md para configurar modelos personalizados.
const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
if (model) {
const success = await pi.setModel(model);
if (!success) {
ctx.ui.notify("No API key for this model", "error");
}
}pi.getThinkingLevel() / pi.setThinkingLevel(nível)
Obtenha ou defina o nível de pensamento. O nível é limitado às capacidades do modelo (modelos sem raciocínio sempre usam "off"). As alterações emitem thinking_level_select.
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
pi.setThinkingLevel("high");pi.eventos
Barramento de eventos compartilhado para comunicação entre ramais:
pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });pi.registerProvider(nome, configuração)
Registre ou substitua um provedor de modelo dinamicamente. Útil para proxies, endpoints personalizados ou configurações de modelo para toda a equipe.
As chamadas feitas durante a função de fábrica do ramal são enfileiradas e aplicadas assim que o executor é inicializado. As chamadas feitas depois disso — por exemplo, de um manipulador de comando seguindo um fluxo de configuração do usuário — entram em vigor imediatamente sem exigir um /reload.
Provedores dinâmicos podem implementar refreshModels. Pi chama-o durante a atualização do modelo, publica a lista retornada de forma síncrona por meio do provedor e passa o contexto canônico de credencial/catálogo armazenado/rede/sinal. A extensão decide se persiste os metadados do catálogo por meio de context.publish({ persist: entry }) com verificação de geração; servidores live como llama.cpp podem retornar modelos sem persisti-los.
context.signal é sempre um sinal concreto e os retornos de chamada do provedor devem passá-lo para bloquear I/O. As chamadas públicas ModelRuntime.refresh() e ModelRegistry.refresh() aceitam um sinal opcional e são ilimitadas quando ele é omitido; extensões e inscrições escolhem seus próprios prazos. O cancelamento interrompe a espera do chamador, mesmo que um provedor ignore o sinal, mas a cooperação ainda é necessária para interromper o trabalho subjacente.
Extensions que precisam de autenticação, filtragem, atualização ou comportamento de fluxo do provedor nativo podem registrar um Provider completo de @earendil-works/pi-ai. O provedor se torna a base da composição e as substituições models.json ainda se aplicam acima dele.
import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";
const provider = createProvider({
id: "local-server",
name: "Local Server",
baseUrl: "http://localhost:8080/v1",
auth: {
apiKey: {
name: "Local server setup",
async login(interaction) {
return {
type: "api_key",
key: await interaction.prompt({ type: "secret", message: "API key" }),
};
},
async resolve({ credential }) {
return credential?.key
? { auth: { apiKey: credential.key }, source: "stored API key" }
: undefined;
},
},
},
models: [],
api: openAICompletionsApi(),
});
pi.registerProvider(provider);
// Register a new provider with custom models
pi.registerProvider("my-proxy", {
name: "My Proxy",
baseUrl: "https://proxy.example.com",
apiKey: "$PROXY_API_KEY", // env var reference
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet (proxy)",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 16384
}
]
});
// Register a live llama.cpp catalog without persisting discovered models
pi.registerProvider("llama.cpp", {
baseUrl: "http://localhost:8080/v1",
apiKey: "local",
api: "openai-completions",
async refreshModels({ signal }) {
const response = await fetch("http://localhost:8080/v1/models", { signal });
const { data } = await response.json();
return data.map(({ id }) => ({
id,
name: id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384
}));
}
});
// Override baseUrl for an existing provider (keeps all models)
pi.registerProvider("anthropic", {
baseUrl: "https://proxy.example.com"
});
// Register provider with OAuth support for /login
pi.registerProvider("corporate-ai", {
baseUrl: "https://ai.corp.com",
api: "openai-responses",
models: [...],
oauth: {
name: "Corporate AI (SSO)",
async login(callbacks) {
// Custom OAuth flow
callbacks.onAuth({ url: "https://sso.corp.com/..." });
const code = await callbacks.onPrompt({ message: "Enter code:" });
return { refresh: code, access: code, expires: Date.now() + 3600000 };
},
async refreshToken(credentials, signal) {
signal.throwIfAborted();
// Refresh logic
return credentials;
},
getApiKey(credentials) {
return credentials.access;
}
}
});O formulário do objeto aceita um pi-ai Provider completo, incluindo comportamento nativo auth, getModels, refreshModels, filterModels, stream e streamSimple.
Opções de configuração legadas:
name- Nome de exibição do provedor na UI, como/login.baseUrl- API URL do terminal. Obrigatório ao definir modelos.apiKey- API key literal, interpolação de ambiente ($ENV_VARou${ENV_VAR}) ou!commandinicial. Obrigatório ao definir modelos (a menos queoauthseja fornecido).$escapa ``apiKey- API key literal, interpolação de ambiente ($ENV_VARou${ENV_VAR}) ou!commandinicial. Obrigatório ao definir modelos (a menos queoauthseja fornecido).$escapa e$!escapa de um literal!` sem acionar a execução do comando.api- API tipo:"anthropic-messages","openai-completions","openai-responses", etc.headers- Cabeçalhos personalizados para incluir nas solicitações.authHeader- Se verdadeiro, adiciona o cabeçalhoAuthorization: Bearerautomaticamente.models- Matriz de definições de modelo. Se fornecido, substitui todos os modelos existentes para este fornecedor. As definições de modelo podem definirbaseUrlpara substituir o terminal do provedor desse modelo.refreshModels- Retorno de chamada de descoberta dinâmica assíncrona. Seus modelos retornados substituem os modelos fornecidos por extensão.context.storedcontém o instantâneo do provedor persistente; usecontext.publish({ persist: entry })com verificação de geração somente quando os dados do catálogo atualizados persistirem. Usepersist: nullpara excluir esse instantâneo.oauth- configuração do provedor OAuth para suporte/login. Quando fornecido, o provedor aparece no menu de login.streamSimple- Implementação de streaming personalizada para APIs não padrão.
Consulte custom-provider.md para tópicos avançados: streaming personalizado APIs, OAuth detalhes, referência de definição de modelo.
pi.unregisterProvider(nome)
Remova um provedor previamente cadastrado e seus modelos. Os modelos integrados que foram substituídos pelo provedor são restaurados. Não tem efeito se o provedor não estiver cadastrado.
Assim como registerProvider, isso entra em vigor imediatamente quando chamado após a fase inicial de carregamento, portanto, /reload não é necessário.
pi.registerCommand("my-setup-teardown", {
description: "Remove the custom proxy provider",
handler: async (_args, _ctx) => {
pi.unregisterProvider("my-proxy");
},
});Gestão Estadual
Extensions com estado deve armazená-lo no resultado da ferramenta details para suporte adequado à ramificação:
export default function (pi: ExtensionAPI) {
let items: string[] = [];
// Reconstruct state from session
pi.on("session_start", async (_event, ctx) => {
items = [];
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type === "message" && entry.message.role === "toolResult") {
if (entry.message.toolName === "my_tool") {
items = entry.message.details?.items ?? [];
}
}
}
});
pi.registerTool({
name: "my_tool",
// ...
async execute(toolCallId, params, signal, onUpdate, ctx) {
items.push("new item");
return {
content: [{ type: "text", text: "Added" }],
details: { items: [...items] }, // Store for reconstruction
};
},
});
}Ferramentas personalizadas
Registre ferramentas que o LLM pode chamar via pi.registerTool(). As ferramentas aparecem no prompt do sistema e podem ter renderização personalizada.
Use promptSnippet para uma entrada curta de uma linha na seção Available tools no prompt padrão do sistema. Se omitido, as ferramentas personalizadas serão deixadas de fora dessa seção.
Use promptGuidelines para adicionar marcadores específicos da ferramenta à seção Guidelines do prompt padrão do sistema. Esses marcadores são incluídos apenas enquanto a ferramenta está ativa (por exemplo, após pi.setActiveTools([...])).
Importante: os marcadores promptGuidelines são anexados na seção Guidelines sem prefixo ou agrupamento de nome de ferramenta. Cada diretriz deve nomear a ferramenta a que se refere - evite "Use esta ferramenta quando..." porque o LLM não pode dizer qual ferramenta "isto" significa. Escreva "Use my_tool quando...".
Nota: Alguns modelos são idiotas e incluem o prefixo @ nos argumentos do caminho da ferramenta. As ferramentas integradas retiram um @ inicial antes de resolver os caminhos. Se sua ferramenta personalizada aceitar um caminho, normalize um @ inicial também.
Se sua ferramenta personalizada modificar arquivos, use withFileMutationQueue() para que ela participe da mesma fila por arquivo que edit e write integrados. Isso é importante porque as chamadas de ferramentas são executadas em paralelo por padrão. Sem a fila, duas ferramentas podem ler o mesmo conteúdo de arquivo antigo, calcular atualizações diferentes e, em seguida, a última gravação sobrescreve a outra.
Exemplo de caso de falha: sua ferramenta personalizada edita foo.ts enquanto o edit integrado também altera foo.ts no mesmo turno do assistente. Se a sua ferramenta não participar da fila, ambas poderão ler o foo.ts original, aplicar alterações separadas e uma dessas alterações será perdida.
Passe o caminho real do arquivo de destino para withFileMutationQueue(), não o argumento bruto do usuário. Resolva-o primeiro para um caminho absoluto, relativo a ctx.cwd ou ao diretório de trabalho da sua ferramenta. Para arquivos existentes, o auxiliar canoniza por meio de realpath(), portanto, os aliases de links simbólicos para o mesmo arquivo compartilham uma fila. Para novos arquivos, ele retorna ao caminho absoluto resolvido porque ainda não há nada para realpath().
Coloque toda a janela de mutação na fila nesse caminho de destino. Isso inclui a lógica de leitura-modificação-gravação, não apenas a gravação final.
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const absolutePath = resolve(ctx.cwd, params.path);
return withFileMutationQueue(absolutePath, async () => {
await mkdir(dirname(absolutePath), { recursive: true });
const current = await readFile(absolutePath, "utf8");
const next = current.replace(params.oldText, params.newText);
await writeFile(absolutePath, next, "utf8");
return {
content: [{ type: "text", text: `Updated ${params.path}` }],
details: {},
};
});
}Definição de ferramenta
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
import { Text } from "@earendil-works/pi-tui";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does (shown to LLM)",
promptSnippet: "List or add items in the project todo list",
promptGuidelines: [
"Use my_tool for todo planning instead of direct file edits when the user asks for a task list."
],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return args;
const input = args as { action?: string; oldAction?: string };
if (typeof input.oldAction === "string" && input.action === undefined) {
return { ...input, action: input.oldAction };
}
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Check for cancellation
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
}
// Stream progress updates
onUpdate?.({
content: [{ type: "text", text: "Working..." }],
details: { progress: 50 },
});
// Run commands via pi.exec (captured from extension closure)
const result = await pi.exec("some-command", [], { signal });
// Return result
return {
content: [{ type: "text", text: "Done" }], // Sent to LLM
details: { data: result }, // For rendering & state
// usage: nestedModelResponse.usage, // Optional nested LLM usage
// Optional: stop after this tool batch when every finalized tool result
// in the batch also returns terminate: true.
terminate: true,
};
},
// Optional: Custom rendering
renderCall(args, theme, context) { ... },
renderResult(result, options, theme, context) { ... },
});Contabilidade de uso: Se uma ferramenta fizer chamadas LLM aninhadas, retorne seu Usage combinado como usage. Pi persiste no resultado da ferramenta e inclui-o no rodapé, /session e RPC totais da sessão. tool_result manipuladores podem inspecionar ou substituir este valor.
Erros de sinalização: Para marcar a execução de uma ferramenta como falhada (definir isError: true no resultado e reportá-lo ao LLM), gere um erro de execute. Retornar um valor nunca define o sinalizador de erro, independentemente das propriedades incluídas no objeto de retorno.
Encerramento antecipado: Retorne terminate: true de execute() para sugerir que a chamada LLM de acompanhamento automático deve ser ignorada após o lote de ferramentas atual. Isso só entra em vigor quando cada resultado de ferramenta finalizado nesse lote estiver sendo finalizado. Consulte examples/extensions/structured-output.ts para obter um exemplo mínimo de onde o agente termina em uma chamada final da ferramenta de saída estruturada.
// Correct: throw to signal an error
async execute(toolCallId, params) {
if (!isValid(params.input)) {
throw new Error(`Invalid input: ${params.input}`);
}
return { content: [{ type: "text", text: "OK" }], details: {} };
}Importante: Use StringEnum de @earendil-works/pi-ai para enumerações de strings. Type.Union/Type.Literal não funciona com API do Google.
Preparação de argumentos: prepareArguments(args) é opcional. Se definido, ele é executado antes da validação do esquema e antes de execute(). Use-o para imitar uma forma de entrada aceita mais antiga quando pi retoma uma sessão mais antiga cujos argumentos de chamada de ferramenta armazenados não correspondem mais ao esquema atual. Retorne o objeto que você deseja validar em parameters. Mantenha o esquema público rigoroso. Não adicione campos de compatibilidade obsoletos a parameters apenas para manter sessões antigas retomadas funcionando.
Exemplo: uma sessão mais antiga pode conter uma chamada de ferramenta edit com oldText e newText de nível superior, enquanto o esquema atual aceita apenas edits: [{ oldText, newText }].
pi.registerTool({
name: "edit",
label: "Edit",
description: "Edit a single file using exact text replacement",
parameters: Type.Object({
path: Type.String(),
edits: Type.Array(
Type.Object({
oldText: Type.String(),
newText: Type.String(),
}),
),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return args;
const input = args as {
path?: string;
edits?: Array<{ oldText: string; newText: string }>;
oldText?: unknown;
newText?: unknown;
};
if (typeof input.oldText !== "string" || typeof input.newText !== "string") {
return args;
}
return {
...input,
edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],
};
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// params now matches the current schema
return {
content: [{ type: "text", text: `Applying ${params.edits.length} edit block(s)` }],
details: {},
};
},
});Substituindo ferramentas integradas
Extensions pode substituir ferramentas integradas (read, bash, edit, write, grep, find, ls) registrando uma ferramenta com o mesmo nome. O modo interativo exibe um aviso quando isso acontece.
# Extension's read tool replaces built-in read
pi -e ./tool-override.tsAlternativamente, use --no-builtin-tools para iniciar sem nenhuma ferramenta integrada, mantendo as ferramentas de extensão habilitadas:
# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.tsVeja examples/extensions/tool-override.ts para um exemplo completo que substitui read pelo registro e controle de acesso.
Renderização: A herança do renderizador integrado é resolvida por slot. A substituição de execução e a substituição de renderização são independentes. Se sua substituição omitir renderCall, o renderCall integrado será usado. Se sua substituição omitir renderResult, o renderResult integrado será usado. Se sua substituição omitir ambos, o renderizador integrado será usado automaticamente (destaque de sintaxe, diferenças, etc.). Isso permite agrupar ferramentas integradas para registro ou controle de acesso sem reimplementar a IU.
Metadados de prompt: promptSnippet e promptGuidelines não são herdados da ferramenta integrada. Se sua substituição deve manter essas instruções imediatas, defina-as explicitamente na substituição.
Sua implementação deve corresponder exatamente ao formato do resultado, incluindo o tipo details. A UI e a lógica da sessão dependem dessas formas para renderização e rastreamento de estado.
Implementações de ferramentas integradas:
- read.ts -
ReadToolDetails - bash.ts -
BashToolDetails - edit.ts
- write.ts
- grep.ts -
GrepToolDetails - find.ts -
FindToolDetails - ls.ts -
LsToolDetails
Execução Remota
Ferramentas integradas suportam operações conectáveis para delegação a sistemas remotos (SSH, contêineres, etc.):
import { createReadTool, createBashTool, type ReadOperations } from "@earendil-works/pi-coding-agent";
// Create tool with custom operations
const remoteRead = createReadTool(cwd, {
operations: {
readFile: (path) => sshExec(remote, `cat ${path}`),
access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
}
});
// Register, checking flag at execution time
pi.registerTool({
...remoteRead,
async execute(id, params, signal, onUpdate, _ctx) {
const ssh = getSshConfig();
if (ssh) {
const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });
return tool.execute(id, params, signal, onUpdate);
}
return localRead.execute(id, params, signal, onUpdate);
},
});Interfaces de operações: ReadOperations, WriteOperations, EditOperations, BashOperations, LsOperations, GrepOperations, FindOperations
Para user_bash, as extensões podem reutilizar o back-end do shell local do pi via createLocalBashOperations() em vez de reimplementar a geração de processos locais, resolução de shell e encerramento da árvore de processos.
A ferramenta bash também suporta um gancho de spawn para ajustar o comando, cwd ou env antes da execução:
import { createBashTool } from "@earendil-works/pi-coding-agent";
const bashTool = createBashTool(cwd, {
spawnHook: ({ command, cwd, env }) => ({
command: `source ~/.profile\n${command}`,
cwd: `/mnt/sandbox${cwd}`,
env: { ...env, CI: "1" },
}),
});createBashTool() expõe a sessão atual aos comandos através de PI_SESSION_ID, PI_SESSION_FILE, PI_PROVIDER, PI_MODEL e PI_REASONING_LEVEL. A injeção acontece antes de spawnHook, então os ganchos recebem esses valores em env e os preservam quando espalham o ambiente existente como acima. Defina exposeSessionEnvironment: false para desativá-los:
const bashTool = createBashTool(cwd, {
exposeSessionEnvironment: false,
});Veja Bash tool session environment para semântica de variáveis. Veja examples/extensions/ssh.ts para um exemplo completo de SSH com flag --ssh.
Truncamento de saída
As ferramentas DEVEM truncar sua saída para evitar sobrecarregar o contexto do LLM. Grandes saídas podem causar:
- Erros de estouro de contexto (prompt muito longo)
- Falhas de compactação
- Desempenho do modelo degradado
O limite integrado é de 50 KB (~10 mil tokens) e 2.000 linhas, o que for atingido primeiro. Use os utilitários de truncamento exportados:
import {
truncateHead, // Keep first N lines/bytes (good for file reads, search results)
truncateTail, // Keep last N lines/bytes (good for logs, command output)
truncateLine, // Truncate a single line to maxBytes with ellipsis
formatSize, // Human-readable size (e.g., "50KB", "1.5MB")
DEFAULT_MAX_BYTES, // 50KB
DEFAULT_MAX_LINES, // 2000
} from "@earendil-works/pi-coding-agent";
async execute(toolCallId, params, signal, onUpdate, ctx) {
const output = await runCommand();
// Apply truncation
const truncation = truncateHead(output, {
maxLines: DEFAULT_MAX_LINES,
maxBytes: DEFAULT_MAX_BYTES,
});
let result = truncation.content;
if (truncation.truncated) {
// Write full output to temp file
const tempFile = writeTempFile(output);
// Inform the LLM where to find complete output
result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
result += ` Full output saved to: ${tempFile}]`;
}
return { content: [{ type: "text", text: result }] };
}Pontos principais:
- Use
truncateHeadpara conteúdo onde o início importa (resultados de pesquisa, leituras de arquivos) - Use
truncateTailpara conteúdo onde o final importa (logs, saída de comando) - Sempre informe o LLM quando a saída estiver truncada e onde encontrar a versão completa
- Documente os limites de truncamento na descrição da sua ferramenta
Veja examples/extensions/truncated-tool.ts para um exemplo completo envolvendo rg (ripgrep) com truncamento adequado.
Várias ferramentas
Uma extensão pode registrar diversas ferramentas com estado compartilhado:
export default function (pi: ExtensionAPI) {
let connection = null;
pi.registerTool({ name: "db_connect", ... });
pi.registerTool({ name: "db_query", ... });
pi.registerTool({ name: "db_close", ... });
pi.on("session_shutdown", async () => {
connection?.close();
});
}Renderização personalizada
As ferramentas podem fornecer renderCall e renderResult para exibição personalizada de TUI. Consulte tui.md para o componente completo API e tool-execution.ts para saber como as linhas de ferramentas são compostas.
Por padrão, a saída da ferramenta é encapsulada em Box que trata do preenchimento e do plano de fundo. Um renderCall ou renderResult definido deve retornar um Component. Se um renderizador de slot não estiver definido, tool-execution.ts usa renderização substituta para esse slot.
Defina renderShell: "self" quando a ferramenta deve renderizar seu próprio shell em vez de usar o padrão Box. Isso é útil para ferramentas que precisam de controle total sobre o enquadramento ou o comportamento do plano de fundo, por exemplo, visualizações grandes que devem permanecer visualmente estáveis após a estabilização da ferramenta.
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "Custom shell example",
parameters: Type.Object({}),
renderShell: "self",
async execute() {
return { content: [{ type: "text", text: "ok" }], details: undefined };
},
renderCall(args, theme, context) {
return new Text(theme.fg("accent", "my custom shell"), 0, 0);
},
});renderCall e renderResult recebem cada um um objeto context com:
args- os argumentos atuais da chamada da ferramentastate- estado local de linha compartilhado entrerenderCallerenderResultlastComponent- o componente retornado anteriormente para esse slot, se houverinvalidate()- solicita uma nova renderização desta linha de ferramentatoolCallId,cwd,executionStarted,argsComplete,isPartial,expanded,showImages,isError
Use context.state para estado compartilhado entre slots. Mantenha caches locais de slot na instância do componente retornado quando quiser reutilizar e alterar o mesmo componente nas renderizações.
renderCall
Renderiza a chamada ou cabeçalho da ferramenta:
import { Text } from "@earendil-works/pi-tui";
renderCall(args, theme, context) {
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
let content = theme.fg("toolTitle", theme.bold("my_tool "));
content += theme.fg("muted", args.action);
if (args.text) {
content += " " + theme.fg("dim", `"${args.text}"`);
}
text.setText(content);
return text;
}renderResult
Renderiza o resultado ou saída da ferramenta:
renderResult(result, { expanded, isPartial }, theme, context) {
if (isPartial) {
return new Text(theme.fg("warning", "Processing..."), 0, 0);
}
if (result.details?.error) {
return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0);
}
let text = theme.fg("success", "✓ Done");
if (expanded && result.details?.items) {
for (const item of result.details.items) {
text += "\n " + theme.fg("dim", item);
}
}
return new Text(text, 0, 0);
}Se um slot intencionalmente não tiver conteúdo visível, retorne um Component vazio, como um Container vazio.
Dicas de atalho de teclado
Use keyHint() para exibir dicas de atalhos de teclado que respeitam a configuração de atalhos de teclado ativa:
import { keyHint } from "@earendil-works/pi-coding-agent";
renderResult(result, { expanded }, theme, context) {
let text = theme.fg("success", "✓ Done");
if (!expanded) {
text += ` (${keyHint("app.tools.expand", "to expand")})`;
}
return new Text(text, 0, 0);
}Funções disponíveis:
keyHint(keybinding, description)- Formata um ID de atalho de teclado configurado, como"app.tools.expand"ou"tui.select.confirm"keyText(keybinding)- Retorna o texto da chave configurada bruta para um ID de atalho de tecladorawKeyHint(key, description)- Formatar uma string de chave bruta
Use IDs de atalhos de teclado com namespace:
- Os IDs do agente de codificação usam o namespace
app.*, por exemploapp.tools.expand,app.editor.external,app.session.rename - Os ids TUI compartilhados usam o namespace
tui.*, por exemplotui.select.confirm,tui.select.cancel,tui.input.tab
Para a lista completa de ids e padrões de atalhos de teclado, consulte keybindings.md. keybindings.json usa os mesmos IDs com namespace.
Editores personalizados e componentes ctx.ui.custom() recebem keybindings: KeybindingsManager como argumento injetado. Eles deveriam usar esse gerenciador injetado diretamente em vez de chamar getKeybindings() ou setKeybindings().
Melhores Práticas
- Use
Textcom preenchimento(0, 0). A caixa padrão lida com o preenchimento. - Use
\npara conteúdo multilinha. - Identificador
isPartialpara progresso de streaming. - Suporte
expandedpara detalhes sob demanda. - Mantenha a visualização padrão compacta.
- Leia
context.argsemrenderResultem vez de copiar argumentos emcontext.state. - Use
context.stateapenas para dados que devem ser compartilhados entre slots de chamadas e resultados. - Reutilize
context.lastComponentquando a mesma instância do componente puder ser atualizada no local. - Use
renderShell: "self"somente quando o shell em caixa padrão atrapalhar. No modo self-shell, a ferramenta é responsável por seu próprio enquadramento, preenchimento e plano de fundo.
Cair pra trás
Se um renderizador de slot não estiver definido ou gerar:
renderCall: Mostra o nome da ferramentarenderResult: Mostra texto bruto decontent
Carregamento dinâmico de ferramentas
Extensions pode registrar muitas ferramentas enquanto mantém ativo apenas um pequeno conjunto inicial. Uma ferramenta pode então adicionar mais ferramentas com pi.setActiveTools() durante a execução. Pi detecta alterações puramente aditivas, registra os nomes de ferramentas recentemente disponíveis no resultado da ferramenta e aplica o conjunto ativo atualizado antes da próxima solicitação de modelo.
Isso funciona com todos os modelos. Models com suporte nativo de carregamento diferido preserva o prefixo de prompt estável e carrega as novas definições na posição do resultado da ferramenta. Outros modelos usam o substituto descrito abaixo.
O ciclo de vida é:
- Registre cada ferramenta com
pi.registerTool()para que apareça empi.getAllTools(). - Mantenha as ferramentas do carregador, como
search_tools, ativas e deixe as ferramentas pesquisáveis inativas. - Durante a execução do carregador, chame
pi.setActiveTools([...currentTools,...matchingTools]). A mudança deve ser aditiva: não remova ferramentas atualmente ativas na mesma chamada. - Pi registra quais ferramentas foram adicionadas no resultado da ferramenta do carregador.
- Antes da próxima resposta do modelo, Pi expõe as definições adicionadas usando carregamento adiado nativo quando suportado, ou a lista de ferramentas ativas normais caso contrário.
Você não precisa retornar referências de ferramentas específicas do provedor ou marcar o carregador como uma ferramenta de pesquisa especial. A troca de ferramenta ativa é o sinal. Os nomes passados para pi.setActiveTools() já devem estar registrados; nomes desconhecidos são ignorados.
Models com carregamento diferido nativo
- Antrópico
- Models: Sonnet, Opus, Fable versão 4.5 ou mais recente (sem Haiku)
- Representação nativa: As definições diferidas usam
defer_loading; o ponto de carregamento usa conteúdotool_reference.
- AbertaAI
- Models:
gpt-5.4e família mais recente - Representação nativa: Pi adiciona itens de cliente
tool_search_calletool_search_outputconcluídos no ponto de carregamento.
- Models:
Para um modelo personalizado ou proxy verificado, a manipulação nativa pode ser habilitada com compat.supportsToolReferences: true para anthropic-messages ou compat.supportsToolSearch: true para openai-responses e openai-codex-responses. Deixe-os desabilitados, a menos que o endpoint e o modelo aceitem o protocolo nativo correspondente.
Comportamento alternativo
Para todos os outros modelos e provedores, a ativação dinâmica ainda funciona: Pi envia a lista completa de ferramentas ativas atualmente normalmente na próxima solicitação. O modelo pode chamar as ferramentas recém-ativadas, mas adicionar suas definições pode invalidar o prefixo de prompt armazenado em cache do provedor.
Pi também utiliza esse recurso seguro quando o conjunto ativo não é puramente aditivo, como a substituição de um grupo de ferramentas por outro. Portanto, as remoções de ferramentas funcionam, mas não utilizam carregamento diferido.
Para obter o melhor comportamento do cache, mantenha a ferramenta de carregamento ativa durante toda a sessão e adicione ferramentas em vez de substituir o conjunto ativo. Observe também que ativar uma ferramenta com promptSnippet ou promptGuidelines reconstrói o prompt do sistema; essa alteração no prompt do sistema pode invalidar o prefixo mesmo quando o provedor oferece suporte a esquemas adiados. Ferramentas carregadas lentamente geralmente devem confiar em sua ferramenta description e omitir metadados de prompt somente ativos.
Exemplo de ferramenta de pesquisa
A extensão a seguir registra duas ferramentas pesquisáveis, remove-as do conjunto ativo inicial e mantém apenas search_tools como seu carregador. O exemplo usa correspondência simples de palavras-chave, mas a implementação de pesquisa poderia usar BM25, embeddings, um catálogo remoto ou roteamento específico do projeto.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
const SEARCHABLE_TOOL_NAMES = new Set(["lookup_weather", "search_issues"]);
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "lookup_weather",
label: "Lookup Weather",
description: "Look up the current weather for a city",
parameters: Type.Object({ city: Type.String() }),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `Weather for ${params.city}: sunny` }],
details: {},
};
},
});
pi.registerTool({
name: "search_issues",
label: "Search Issues",
description: "Search project issues by keyword",
parameters: Type.Object({ query: Type.String() }),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `No open issues matching ${params.query}` }],
details: {},
};
},
});
pi.registerTool({
name: "search_tools",
label: "Search Tools",
description: "Search for and enable tools relevant to a task",
promptSnippet: "Search for additional tools when the active tools cannot perform the task",
promptGuidelines: [
"Use search_tools when a task requires a capability that is not currently available.",
],
parameters: Type.Object({
query: Type.String({ description: "Capability or task to search for" }),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
}),
async execute(_toolCallId, params) {
const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
const matches = pi.getAllTools()
.filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))
.map((tool) => ({
tool,
score: terms.reduce(
(score, term) =>
score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),
0,
),
}))
.filter((match) => match.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, params.limit ?? 3)
.map((match) => match.tool.name);
if (matches.length === 0) {
return {
content: [{ type: "text", text: `No tools found for: ${params.query}` }],
details: { matches: [] },
};
}
const active = pi.getActiveTools();
const added = matches.filter((name) => !active.includes(name));
pi.setActiveTools([...new Set([...active, ...added])]);
return {
content: [{
type: "text",
text: added.length > 0
? `Loaded tools: ${added.join(", ")}`
: `Matching tools already active: ${matches.join(", ")}`,
}],
details: { matches, added },
};
},
});
pi.on("session_start", () => {
// Keep searchable tools registered but initially inactive. Preserve built-ins
// and tools owned by other extensions, and keep the loader itself active.
const initialTools = pi.getActiveTools().filter(
(name) => !SEARCHABLE_TOOL_NAMES.has(name),
);
pi.setActiveTools([...new Set([...initialTools, "search_tools"])]);
});
}Quando search_tools adiciona uma correspondência, o modelo recebe essa definição na solicitação imediatamente seguinte. Em um modelo com capacidade nativa, a definição é ancorada após o resultado da pesquisa sem alterar o prefixo do esquema de ferramenta inicial. Em outros modelos, ele aparece na lista normal de ferramentas na mesma solicitação seguinte.
IU personalizada
Extensions pode interagir com os usuários por meio de métodos ctx.ui e personalizar como as mensagens/ferramentas são renderizadas.
Para componentes personalizados, consulte tui.md que possui padrões de copiar e colar para:
- Diálogos de seleção (SelectList)
- Operações assíncronas com cancelamento (BorderedLoader)
- Alternância de configurações (SettingsList)
- Indicadores de status (setStatus)
- Mensagem de trabalho, visibilidade e indicador durante a transmissão (
setWorkingMessage,setWorkingVisible,setWorkingIndicator) - Editor de widgets acima/abaixo (setWidget)
- Provedores de preenchimento automático em camadas sobre a conclusão de barra/caminho integrada (addAutocompleteProvider)
- Rodapés personalizados (setFooter)
Diálogos
// Select from options
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
// Confirm dialog
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
// Text input
const name = await ctx.ui.input("Name:", "placeholder");
// Multi-line editor
const text = await ctx.ui.editor("Edit:", "prefilled text");
// Notification (non-blocking)
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"Diálogos cronometrados com contagem regressiva
As caixas de diálogo suportam uma opção timeout que é descartada automaticamente com uma exibição de contagem regressiva ao vivo:
// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
const confirmed = await ctx.ui.confirm(
"Timed Confirmation",
"This dialog will auto-cancel in 5 seconds. Confirm?",
{ timeout: 5000 }
);
if (confirmed) {
// User confirmed
} else {
// User cancelled or timed out
}Valores retornados no tempo limite:
select()retornaundefinedconfirm()retornafalseinput()retornaundefined
Demissão manual com AbortSignal
Para obter mais controle (por exemplo, para distinguir o tempo limite do cancelamento do usuário), use AbortSignal:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const confirmed = await ctx.ui.confirm(
"Timed Confirmation",
"This dialog will auto-cancel in 5 seconds. Confirm?",
{ signal: controller.signal }
);
clearTimeout(timeoutId);
if (confirmed) {
// User confirmed
} else if (controller.signal.aborted) {
// Dialog timed out
} else {
// User cancelled (pressed Escape or selected "No")
}Veja examples/extensions/timed-confirm.ts para exemplos completos.
Widgets, status e rodapé
// Status in footer (persistent until cleared)
ctx.ui.setStatus("my-ext", "Processing...");
ctx.ui.setStatus("my-ext", undefined); // Clear
// Working loader (shown during streaming)
ctx.ui.setWorkingMessage("Thinking deeply...");
ctx.ui.setWorkingMessage(); // Restore default
ctx.ui.setWorkingVisible(false); // Hide the built-in working loader row entirely
ctx.ui.setWorkingVisible(true); // Show the built-in working loader row
// Working indicator (shown during streaming)
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] }); // Static dot
ctx.ui.setWorkingIndicator({
frames: [
ctx.ui.theme.fg("dim", "·"),
ctx.ui.theme.fg("muted", "•"),
ctx.ui.theme.fg("accent", "●"),
ctx.ui.theme.fg("muted", "•"),
],
intervalMs: 120,
});
ctx.ui.setWorkingIndicator({ frames: [] }); // Hide indicator
ctx.ui.setWorkingIndicator(); // Restore default spinner
// Widget above editor (default)
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
// Widget below editor
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));
ctx.ui.setWidget("my-widget", undefined); // Clear
// Custom footer (replaces built-in footer entirely)
ctx.ui.setFooter((tui, theme) => ({
render(width) { return [theme.fg("dim", "Custom footer")]; },
invalidate() {},
}));
ctx.ui.setFooter(undefined); // Restore built-in footer
// Terminal title
ctx.ui.setTitle("pi - my-project");
// Editor text
ctx.ui.setEditorText("Prefill text");
const current = ctx.ui.getEditorText();
// Paste into editor (triggers paste handling, including collapse for large content)
ctx.ui.pasteToEditor("pasted content");
// Stack custom autocomplete behavior on top of the built-in provider
ctx.ui.addAutocompleteProvider((current) => ({
triggerCharacters: ["#"],
async getSuggestions(lines, line, col, options) {
const beforeCursor = (lines[line] ?? "").slice(0, col);
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
if (!match) {
return current.getSuggestions(lines, line, col, options);
}
return {
prefix: `#${match[1] ?? ""}`,
items: [{ value: "#2983", label: "#2983", description: "Extension API for autocomplete" }],
};
},
applyCompletion(lines, line, col, item, prefix) {
return current.applyCompletion(lines, line, col, item, prefix);
},
shouldTriggerFileCompletion(lines, line, col) {
return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;
},
}));
// Tool output expansion
const wasExpanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(true);
ctx.ui.setToolsExpanded(wasExpanded);
// Custom editor (vim mode, emacs mode, etc.)
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
const currentEditor = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))
);
ctx.ui.setEditorComponent(undefined); // Restore default editor
// Theme management (see themes.md for creating themes)
const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...]
const lightTheme = ctx.ui.getTheme("light"); // Load without switching
const result = ctx.ui.setTheme("light"); // Switch by name
if (!result.success) {
ctx.ui.notify(`Failed: ${result.error}`, "error");
}
ctx.ui.setTheme(lightTheme!); // Or switch by Theme object
ctx.ui.theme.fg("accent", "styled text"); // Access current themeOs quadros de indicadores de trabalho personalizados são renderizados literalmente. Se você quiser cores, adicione-as você mesmo às strings do quadro, por exemplo, com ctx.ui.theme.fg(...).
Preenchimento automático Providers
Use ctx.ui.addAutocompleteProvider() para empilhar a lógica de preenchimento automático personalizada sobre o comando de barra integrado e o provedor de caminho. Defina triggerCharacters para gatilhos naturais personalizados, como Use ctx.ui.addAutocompleteProvider()para empilhar a lógica de preenchimento automático personalizada sobre o comando de barra integrado e o provedor de caminho. DefinatriggerCharacters` para gatilhos naturais personalizados, como.
Padrão típico:
- inspecionar o texto antes do cursor
- retorne suas próprias sugestões quando a sintaxe específica da extensão corresponder
- caso contrário, delegue para
current.getSuggestions(...) - delegar
applyCompletion(...)a menos que você precise de um comportamento de inserção personalizado
pi.on("session_start", (_event, ctx) => {
ctx.ui.addAutocompleteProvider((current) => ({
triggerCharacters: ["#"],
async getSuggestions(lines, cursorLine, cursorCol, options) {
const line = lines[cursorLine] ?? "";
const beforeCursor = line.slice(0, cursorCol);
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
if (!match) {
return current.getSuggestions(lines, cursorLine, cursorCol, options);
}
return {
prefix: `#${match[1] ?? ""}`,
items: [
{ value: "#2983", label: "#2983", description: "Extension API for registering custom @ autocomplete providers" },
{ value: "#2753", label: "#2753", description: "Reload stale resource settings" },
],
};
},
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
},
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
},
}));
});Veja github-issue-autocomplete.ts para um exemplo completo que pré-carrega os últimos problemas GitHub abertos com gh issue list e os filtra localmente para conclusão rápida de #.... Requer GitHub CLI (gh) e um checkout de repositório GitHub.
Componentes personalizados
Para UI complexa, use ctx.ui.custom(). Isso substitui temporariamente o editor pelo seu componente até que done() seja chamado:
import { Text, Component } from "@earendil-works/pi-tui";
const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);
text.onKey = (key) => {
if (key === "return") done(true);
if (key === "escape") done(false);
return true;
};
return text;
});
if (result) {
// User pressed Enter
}O retorno de chamada recebe:
tui- TUI instância (para dimensões da tela, gerenciamento de foco)theme- Tema atual para estilokeybindings- Gerenciador de atalhos de teclado do aplicativo (para verificar atalhos)done(value)- Chamada para fechar componente e retornar valor
Veja tui.md para o componente completo API.
Modo de sobreposição (experimental)
Passe { overlay: true } para renderizar o componente como um modal flutuante sobre o conteúdo existente, sem limpar a tela:
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{ overlay: true }
);Para posicionamento avançado (âncoras, margens, porcentagens, visibilidade responsiva), passe overlayOptions. Use onHandle para controlar o foco ou a visibilidade programaticamente:
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{
overlay: true,
overlayOptions: { anchor: "top-right", width: "50%", margin: 2 },
onHandle: (handle) => {
handle.focus(); // focus this overlay and bring it to the visual front
// handle.unfocus({ target: editorComponent }); // release input to a specific component
// handle.setHidden(true/false); // toggle visibility
// handle.hide(); // permanently remove
}
}
);Uma sobreposição visível focada pode recuperar a entrada após o fechamento da UI personalizada temporária sem sobreposição. Se você quiser intencionalmente que outro componente mantenha a entrada enquanto a sobreposição permanece visível, chame handle.unfocus({ target }). Passar { target: null } libera a sobreposição sem focar outro componente.
Veja tui.md para OverlayOptions completo e OverlayHandle API e overlay-qa-tests.ts para exemplos.
Editor personalizado
Substitua o editor de entrada principal por uma implementação personalizada (modo vim, modo emacs, etc.):
import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { matchesKey } from "@earendil-works/pi-tui";
class VimEditor extends CustomEditor {
private mode: "normal" | "insert" = "insert";
handleInput(data: string): void {
if (matchesKey(data, "escape") && this.mode === "insert") {
this.mode = "normal";
return;
}
if (this.mode === "normal" && data === "i") {
this.mode = "insert";
return;
}
super.handleInput(data); // App keybindings + text editing
}
}
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new VimEditor(tui, theme, keybindings)
);
});
}Pontos principais:
- Estenda
CustomEditor(não baseEditor) para obter atalhos de teclado do aplicativo (escape para abortar, ctrl+d, troca de modelo) - Ligue para
super.handleInput(data)para chaves que você não manuseia - A fábrica recebe
tui,themeekeybindingsdo aplicativo - Use
ctx.ui.getEditorComponent()antes desetEditorComponent()para agrupar o editor personalizado configurado anteriormente - Passe
undefinedpara restaurar o padrão:ctx.ui.setEditorComponent(undefined)
Para compor com outra extensão que já substituiu o editor, capture a fábrica anterior antes de configurar a sua:
const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);Veja tui.md Padrão 7 para um exemplo completo com indicador de modo.
Renderização de mensagens e entradas
Registre um renderizador personalizado para mensagens com seu customType. Use renderizadores de mensagens para conteúdo que deve participar do contexto LLM:
import { Text } from "@earendil-works/pi-tui";
pi.registerMessageRenderer("my-extension", (message, options, theme) => {
const { expanded, outputPad } = options;
let text = theme.fg("accent", `[${message.customType}] `);
text += message.content;
if (expanded && message.details) {
text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
}
return new Text(text, outputPad, 0);
});As mensagens são enviadas via pi.sendMessage():
pi.sendMessage({
customType: "my-extension", // Matches registerMessageRenderer
content: "Status update",
display: true, // Show in TUI
details: { ... }, // Available in renderer
});Para conteúdo somente TUI que não deve ser enviado ao LLM, renderize entradas personalizadas:
pi.registerEntryRenderer("my-card", (entry, options, theme) => {
return new Text(theme.fg("accent", JSON.stringify(entry.data)));
});
pi.appendEntry("my-card", { status: "done" });Cores do tema
Todas as funções de renderização recebem um objeto theme. Consulte themes.md para criar temas personalizados e a paleta de cores completa.
// Foreground colors
theme.fg("toolTitle", text) // Tool names
theme.fg("accent", text) // Highlights
theme.fg("success", text) // Success (green)
theme.fg("error", text) // Errors (red)
theme.fg("warning", text) // Warnings (yellow)
theme.fg("muted", text) // Secondary text
theme.fg("dim", text) // Tertiary text
// Text styles
theme.bold(text)
theme.italic(text)
theme.strikethrough(text)Para realce de sintaxe em renderizadores de ferramentas personalizadas:
import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";
// Highlight code with explicit language
const highlighted = highlightCode("const x = 1;", "typescript", theme);
// Auto-detect language from file path
const lang = getLanguageFromPath("/path/to/file.rs"); // "rust"
const highlighted = highlightCode(code, lang, theme);Tratamento de erros
- Erros de extensão são registrados, o agente continua
tool_callerros bloqueiam a ferramenta (à prova de falhas)- Erros da ferramenta
executedevem ser sinalizados por arremesso; o erro gerado é detectado, relatado ao LLM comisError: truee a execução continua
Comportamento do modo
| Modo | ctx.mode |
ctx.hasUI |
Notas |
|---|---|---|---|
| Interativo | "tui" |
true |
TUI completo com renderização de terminal |
RPC (--mode rpc) |
"rpc" |
true |
Diálogos e notificações via protocolo JSON; custom() retorna undefined. Veja rpc.md |
JSON (--mode json) |
"json" |
false |
Fluxo de eventos para stdout; Os métodos de UI são autônomos |
Imprimir (-p) |
"print" |
false |
Extensions executa mas não consegue avisar |
Use ctx.mode === "tui" antes de recursos específicos de TUI (custom(), fábricas de componentes, entrada de terminal). Use ctx.hasUI antes dos métodos de diálogo e notificação que funcionam nos modos TUI e RPC.
Referência de exemplos
Todos os exemplos em examples/extensions/.
| Exemplo | Descrição | Chave APIs |
|---|---|---|
| Ferramentas | ||
hello.ts |
Registro mínimo de ferramenta | registerTool |
question.ts |
Ferramenta com interação do usuário | registerTool, ui.select |
questionnaire.ts |
Ferramenta de assistente de várias etapas | registerTool, ui.custom |
todo.ts |
Ferramenta stateful com persistência | registerTool, appendEntry, renderResult, eventos de sessão |
dynamic-tools.ts |
Registrar ferramentas após inicialização e durante comandos | registerTool, session_start, registerCommand |
structured-output.ts |
Ferramenta final de saída estruturada com terminate: true |
registerTool, finalizando resultados da ferramenta |
truncated-tool.ts |
Exemplo de truncamento de saída | registerTool, truncateHead |
tool-override.ts |
Substituir ferramenta de leitura integrada | registerTool (mesmo nome do integrado) |
| Comandos | ||
pirate.ts |
Modificar prompt do sistema por turno | registerCommand, before_agent_start |
summarize.ts |
Comando de resumo de conversa | registerCommand, ui.custom |
handoff.ts |
Transferência de modelo entre provedores | registerCommand, ui.editor, ui.custom |
qna.ts |
Perguntas e respostas com interface personalizada | registerCommand, ui.custom, setEditorText |
send-user-message.ts |
Injetar mensagens do usuário | registerCommand, sendUserMessage |
reload-runtime.ts |
Comando de recarga e transferência de ferramenta LLM | registerCommand, ctx.reload(), sendUserMessage |
shutdown-command.ts |
Comando de desligamento elegante | registerCommand, shutdown() |
| Eventos e portões | ||
permission-gate.ts |
Bloqueie comandos perigosos | on("tool_call"), ui.confirm |
project-trust.ts |
Decidir ou adiar a confiança do projeto de um usuário/global ou extensão CLI | on("project_trust"), UI confiável, resultado de confiança necessário |
protected-paths.ts |
Bloquear gravações em caminhos específicos | on("tool_call") |
confirm-destructive.ts |
Confirmar alterações de sessão | on("session_before_switch"), on("session_before_fork") |
dirty-repo-guard.ts |
Avisar sobre repositório git sujo | on("session_before_*"), exec |
input-transform.ts |
Transformar a entrada do usuário | on("input") |
input-transform-streaming.ts |
Transformação de entrada com reconhecimento de streaming | on("input"), streamingBehavior |
model-status.ts |
React para modelar mudanças | on("model_select"), setStatus |
provider-payload.ts |
Inspecione cargas úteis e cabeçalhos de resposta do provedor | on("before_provider_request"), on("after_provider_response") |
system-prompt-header.ts |
Exibir informações de prompt do sistema | on("agent_start"), getSystemPrompt |
claude-rules.ts |
Carregar regras de arquivos | on("session_start"), on("before_agent_start") |
prompt-customizer.ts |
Adicione orientação de ferramenta sensível ao contexto usando systemPromptOptions |
on("before_agent_start"), BuildSystemPromptOptions |
file-trigger.ts |
O observador de arquivos aciona mensagens | sendMessage |
| Compactação e Sessões | ||
custom-compaction.ts |
Resumo de compactação personalizado | on("session_before_compact") |
trigger-compact.ts |
Acionar a compactação manualmente | compact() |
git-checkpoint.ts |
Git estoque em turnos | on("turn_start"), on("session_before_fork"), exec |
git-merge-and-resolve.ts |
Buscar, mesclar e resolver conflitos | on("agent_end"), exec, sendUserMessage |
auto-commit-on-exit.ts |
Confirmar no desligamento | on("session_shutdown"), exec |
| Componentes da IU | ||
status-line.ts |
Indicador de status do rodapé | setStatus, eventos de sessão |
working-indicator.ts |
Personalize o indicador de funcionamento do streaming | setWorkingIndicator, registerCommand |
github-issue-autocomplete.ts |
Adicione conclusões de problemas #1234 além do preenchimento automático integrado, pré-carregando problemas abertos recentes de gh issue list |
addAutocompleteProvider, on("session_start"), exec |
custom-footer.ts |
Substitua totalmente o rodapé | registerCommand, setFooter |
custom-header.ts |
Substituir cabeçalho de inicialização | on("session_start"), setHeader |
modal-editor.ts |
Editor modal estilo Vim | setEditorComponent, CustomEditor |
rainbow-editor.ts |
Estilo de editor personalizado | setEditorComponent |
widget-placement.ts |
Editor de widget acima/abaixo | setWidget |
overlay-test.ts |
Componentes de sobreposição | ui.custom com opções de sobreposição |
overlay-qa-tests.ts |
Testes de sobreposição abrangentes | ui.custom, todas as opções de sobreposição |
notify.ts |
Notificações simples | ui.notify |
timed-confirm.ts |
Diálogos com tempo limite | ui.confirm com tempo limite/sinal |
mac-system-theme.ts |
Tema de troca automática | setTheme, exec |
| Complexo Extensions | ||
plan-mode/ |
Implementação completa do modo de plano | Todos os tipos de eventos, registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools |
preset.ts |
Predefinições salváveis (modelo, ferramentas, pensamento) | registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry |
tools.ts |
Ativar/desativar ferramentas da interface do usuário | registerCommand, setActiveTools, SettingsList, eventos de sessão |
| Remoto e Sandbox | ||
ssh.ts |
SSH execução remota | registerFlag, on("user_bash"), on("before_agent_start"), operações de ferramenta |
interactive-shell.ts |
Sessão de shell persistente | on("user_bash") |
sandbox/ |
Execução de ferramenta em sandbox | Operações de ferramentas |
gondolin/ |
Roteie ferramentas integradas e comandos ! para uma micro-VM Gondolin |
Operações de ferramentas, substituições de ferramentas integradas, on("user_bash") |
subagent/ |
Gerar subagentes | registerTool, exec |
| Jogos | ||
snake.ts |
Jogo de cobra | registerCommand, ui.custom, manuseio do teclado |
space-invaders.ts |
Jogo Invasores do Espaço | registerCommand, ui.custom |
doom-overlay/ |
Perdição em sobreposição | ui.custom com sobreposição |
| Providers | ||
custom-provider-anthropic/ |
Proxy antrópico personalizado | registerProvider |
custom-provider-gitlab-duo/ |
GitIntegração do Lab Duo | registerProvider com OAuth |
| Mensagens e comunicação | ||
message-renderer.ts |
Renderização de mensagem personalizada | registerMessageRenderer, sendMessage |
entry-renderer.ts |
TUI renderização de entrada personalizada somente | registerEntryRenderer, appendEntry |
event-bus.ts |
Eventos entre extensões | pi.events |
| Metadados da sessão | ||
session-name.ts |
Nomear sessões para o seletor | setSessionName, getSessionName |
bookmark.ts |
Marcar entradas para /tree | setLabel |
| Diversos | ||
inline-bash.ts |
Inline bash em chamadas de ferramenta | on("tool_call") |
bash-spawn-hook.ts |
Ajuste o comando bash, cwd e env antes da execução | createBashTool, spawnHook |
with-deps/ |
Extensão com dependências npm | Estrutura do pacote com package.json |