Configuração, personalização, ajustes de plataforma e referências de API para Pi.

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. Use pi -e./path.ts apenas 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 /mycommand via pi.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

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

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

Diretó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 module

Pacote 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_shutdown

Eventos 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_start e message_end disparam para mensagens de usuário, assistente e toolResult.
  • message_update dispara para atualizações de streaming do assistente.
  • Os manipuladores message_end podem retornar { message } para substituir a mensagem finalizada. A substituição deve manter o mesmo role.
pi.on("message_start", async (event, ctx) => {
  // event.message
});

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

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

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

tool_execution_start / tool_execution_update / tool_execution_end

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ção
  • tool_execution_update eventos podem intercalar-se entre ferramentas
  • tool_execution_end é emitido na ordem de conclusão da ferramenta após cada ferramenta ser finalizada
  • eventos de mensagem final toolResult ainda 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.input afetam a execução real da ferramenta
  • Os manipuladores tool_call posteriores veem as mutações feitas pelos manipuladores anteriores
  • Nenhuma revalidação é realizada após sua mutação
  • Retornar valores do bloqueio de controle tool_call via { block: true, reason?: string, terminate?: boolean }
  • terminate aplica-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, isError ou usage); 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:

  1. Comandos de extensão (/cmd) verificados primeiro - se encontrados, o manipulador é executado e o evento de entrada é ignorado
  2. input eventos disparados - podem interceptar, transformar ou manipular
  3. Se não for tratado: comandos de habilidade (/skill:name) expandidos para conteúdo de habilidade
  4. Se não for tratado: prompt templates (/template) expandido para o conteúdo do modelo
  5. 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ão
  • handled - 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 ID

ctx.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 context posteriores.
  • 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ão
  • setup: altera o SessionManager da nova sessão antes de withSession ser executado
  • withSession: execute o trabalho pós-troca em um novo contexto de sessão de substituição. Não use o antigo pi / comando ctx capturado; 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 editor
  • position: "at" duplica o caminho ativo através da entrada selecionada sem restaurar o texto do editor
  • withSession: execute o trabalho pós-troca em um novo contexto de sessão de substituição. Não use o antigo pi / comando ctx capturado; 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 abandonada
  • customInstructions: Instruções personalizadas para o resumidor
  • replaceInstructions: Se verdadeiro, customInstructions substitui o prompt padrão em vez de ser anexado
  • label: 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:

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 emitiu session_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á recebeu session_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 withSession iniciar.
  • Objetos antigos pi / comando antigo ctx capturados e vinculados à sessão ficam obsoletos após a substituição e serão lançados se usados. Use apenas ctx passado para withSession para trabalho vinculado à sessão.
  • Objetos brutos extraídos anteriormente ainda são de sua responsabilidade. Por exemplo, se você capturar const sm = ctx.sessionManager antes da substituição, sm ainda será o antigo objeto SessionManager. Não o reutilize após a substituição.
  • O código em withSession deve assumir que qualquer estado invalidado pelo seu manipulador session_shutdown já 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() emite session_shutdown para o tempo de execução da extensão atual
  • Em seguida, recarrega recursos e emite session_start com reason: "reload" e resources_discover com 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"
  • isStreamingtrue para atualizações parciais do assistente; false para usuário, assistente finalizado e mensagens restauradas
  • availableWidth — 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.killed

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

pi.getAllTools() retorna name, description, parameters, promptGuidelines e sourceInfo.

Valores típicos de sourceInfo.source:

  • builtin para ferramentas integradas
  • sdk para ferramentas passadas via createAgentSession({ 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_VAR ou ${ENV_VAR}) ou !command inicial. Obrigatório ao definir modelos (a menos que oauth seja 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çalho Authorization: Bearer automaticamente.
  • models - Matriz de definições de modelo. Se fornecido, substitui todos os modelos existentes para este fornecedor. As definições de modelo podem definir baseUrl para 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.stored contém o instantâneo do provedor persistente; use context.publish({ persist: entry }) com verificação de geração somente quando os dados do catálogo atualizados persistirem. Use persist: null para 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.ts

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

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

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 truncateHead para conteúdo onde o início importa (resultados de pesquisa, leituras de arquivos)
  • Use truncateTail para 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 ferramenta
  • state - estado local de linha compartilhado entre renderCall e renderResult
  • lastComponent - o componente retornado anteriormente para esse slot, se houver
  • invalidate() - solicita uma nova renderização desta linha de ferramenta
  • toolCallId, 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 teclado
  • rawKeyHint(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 exemplo app.tools.expand, app.editor.external, app.session.rename
  • Os ids TUI compartilhados usam o namespace tui.*, por exemplo tui.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 Text com preenchimento (0, 0). A caixa padrão lida com o preenchimento.
  • Use \n para conteúdo multilinha.
  • Identificador isPartial para progresso de streaming.
  • Suporte expanded para detalhes sob demanda.
  • Mantenha a visualização padrão compacta.
  • Leia context.args em renderResult em vez de copiar argumentos em context.state.
  • Use context.state apenas para dados que devem ser compartilhados entre slots de chamadas e resultados.
  • Reutilize context.lastComponent quando 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 ferramenta
  • renderResult: Mostra texto bruto de content

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

  1. Registre cada ferramenta com pi.registerTool() para que apareça em pi.getAllTools().
  2. Mantenha as ferramentas do carregador, como search_tools, ativas e deixe as ferramentas pesquisáveis ​​inativas.
  3. 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.
  4. Pi registra quais ferramentas foram adicionadas no resultado da ferramenta do carregador.
  5. 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údo tool_reference.
  • AbertaAI
    • Models: gpt-5.4 e família mais recente
    • Representação nativa: Pi adiciona itens de cliente tool_search_call e tool_search_output concluídos no ponto de carregamento.

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

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 theme

Os 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 estilo
  • keybindings - 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 base Editor) 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, theme e keybindings do aplicativo
  • Use ctx.ui.getEditorComponent() antes de setEditorComponent() para agrupar o editor personalizado configurado anteriormente
  • Passe undefined para 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_call erros bloqueiam a ferramenta (à prova de falhas)
  • Erros da ferramenta execute devem ser sinalizados por arremesso; o erro gerado é detectado, relatado ao LLM com isError: true e 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